This commit is contained in:
woodser 2021-05-04 20:20:01 -04:00
commit 8a38081c04
2800 changed files with 344130 additions and 0 deletions

26
.editorconfig Normal file
View File

@ -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

15
.gitattributes vendored Normal file
View File

@ -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

45
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@ -0,0 +1,45 @@
---
name: "\U0001F41B Bug report"
about: Report a bug or a technical issue
---
<!--
SUPPORT REQUESTS: This is for reporting bugs in the Bisq app.
If you have a support request, please join #support on Bisq's
Keybase team over at https://keybase.io/team/Bisq
-->
### Description
<!-- brief description of the bug -->
#### Version
<!-- commit id or version number -->
### Steps to reproduce
<!--if you can reliably reproduce the bug, list the steps here -->
### Expected behaviour
<!--description of the expected behavior -->
### Actual behaviour
<!-- explain what happened instead of the expected behaviour -->
### Screenshots
<!--Screenshots if gui related, drag and drop to add to the issue -->
#### Device or machine
<!-- device/machine used, operating system -->
#### Additional info
<!-- Additional information useful for debugging (e.g. logs) -->

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -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

18
.github/ISSUE_TEMPLATE/new_asset.md vendored Normal file
View File

@ -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)_

15
.github/boring-cyborg.yml vendored Normal file
View File

@ -0,0 +1,15 @@
labelPRBasedOnFilePath:
in:altcoins:
- assets/**/*
is:no-priority:
- assets/**/*
firstPRWelcomeComment: >
**Thanks for opening this pull request!**<br/><br/>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).<br/><br/>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!**<br/><br/>Be sure to follow the issue template. Your issue will be reviewed by a maintainer and labeled for further action.

35
.github/stale.yml vendored Normal file
View File

@ -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.

37
.gitignore vendored Normal file
View File

@ -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*

View File

@ -0,0 +1,192 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_ATTRIBUTE_WRAP" value="0" />
<option name="HTML_TEXT_WRAP" value="0" />
<option name="HTML_ALIGN_ATTRIBUTES" value="false" />
</HTMLCodeStyleSettings>
<JavaCodeStyleSettings>
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="5" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="bisq.monitor" withSubpackages="true" static="false" />
<emptyLine />
<package name="bisq.price" withSubpackages="true" static="false" />
<emptyLine />
<package name="bisq.statistics" withSubpackages="true" static="false" />
<emptyLine />
<package name="bisq.seednode" withSubpackages="true" static="false" />
<emptyLine />
<package name="bisq.desktop" withSubpackages="true" static="false" />
<emptyLine />
<package name="bisq.core" withSubpackages="true" static="false" />
<emptyLine />
<package name="bisq.asset" withSubpackages="true" static="false" />
<emptyLine />
<package name="bisq.network" withSubpackages="true" static="false" />
<emptyLine />
<package name="bisq.common" withSubpackages="true" static="false" />
<emptyLine />
<package name="bisq.asset" withSubpackages="true" static="false" />
<emptyLine />
<package name="bisq.proto.grpc" withSubpackages="true" static="false" />
<emptyLine />
<package name="protobuf" withSubpackages="true" static="false" />
<emptyLine />
<package name="io.grpc" withSubpackages="true" static="false" />
<emptyLine />
<package name="com.google.protobuf" withSubpackages="true" static="false" />
<emptyLine />
<package name="net.gpedro.integrations.slack" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.knowm.xchange" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.libdohj" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.bitcoinj" withSubpackages="true" static="false" />
<emptyLine />
<package name="com.neemre.btcdcli4j" withSubpackages="true" static="false" />
<emptyLine />
<package name="net.glxn.qrgen" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.springframework" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.apache.http" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax.servlet" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.berndpruenster.netlayer" withSubpackages="true" static="false" />
<emptyLine />
<package name="com.runjva" withSubpackages="true" static="false" />
<emptyLine />
<package name="com.googlecode.jcsv" withSubpackages="true" static="false" />
<emptyLine />
<package name="com.fasterxml.jackson" withSubpackages="true" static="false" />
<emptyLine />
<package name="com.google.gson" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.json.simple" withSubpackages="true" static="false" />
<emptyLine />
<package name="joptsimple" withSubpackages="true" static="false" />
<emptyLine />
<package name="com.google.inject" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax.inject" withSubpackages="true" static="false" />
<emptyLine />
<package name="com.google.common" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.apache.commons" withSubpackages="true" static="false" />
<emptyLine />
<package name="de.jensd.fx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com.jfoenix" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.controlsfx" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.reactfx" withSubpackages="true" static="false" />
<emptyLine />
<package name="javafx.application" withSubpackages="true" static="false" />
<emptyLine />
<package name="javafx.fxml" withSubpackages="true" static="false" />
<emptyLine />
<package name="javafx.animation" withSubpackages="true" static="false" />
<emptyLine />
<package name="javafx.stage" withSubpackages="true" static="false" />
<emptyLine />
<package name="javafx.scene" withSubpackages="true" static="false" />
<emptyLine />
<package name="com.sun.javafx.scene" withSubpackages="true" static="false" />
<emptyLine />
<package name="com.sun.javafx.tk.quantum" withSubpackages="true" static="false" />
<emptyLine />
<package name="javafx.css" withSubpackages="true" static="false" />
<emptyLine />
<package name="com.sun.javafx.css" withSubpackages="true" static="false" />
<emptyLine />
<package name="javafx.geometry" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.fxmisc.easybind" withSubpackages="true" static="false" />
<emptyLine />
<package name="javafx.beans" withSubpackages="true" static="false" />
<emptyLine />
<package name="javafx.event" withSubpackages="true" static="false" />
<emptyLine />
<package name="javafx.collections" withSubpackages="true" static="false" />
<emptyLine />
<package name="com.sun.javafx.collections" withSubpackages="true" static="false" />
<emptyLine />
<package name="javafx.util" withSubpackages="true" static="false" />
<emptyLine />
<package name="java.awt" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax.imageio" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.spongycastle" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.bouncycastle" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax.crypto" withSubpackages="true" static="false" />
<emptyLine />
<package name="java.security" withSubpackages="true" static="false" />
<emptyLine />
<package name="java.time" withSubpackages="true" static="false" />
<emptyLine />
<package name="java.text" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax.net" withSubpackages="true" static="false" />
<emptyLine />
<package name="java.net" withSubpackages="true" static="false" />
<emptyLine />
<package name="java.nio" withSubpackages="true" static="false" />
<emptyLine />
<package name="java.io" withSubpackages="true" static="false" />
<emptyLine />
<package name="java.math" withSubpackages="true" static="false" />
<emptyLine />
<package name="java.util" withSubpackages="true" static="false" />
<emptyLine />
<package name="java.lang" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.slf4j" withSubpackages="true" static="false" />
<emptyLine />
<package name="ch.qos.logback" withSubpackages="true" static="false" />
<emptyLine />
<package name="lombok" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.jetbrains.annotations" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax.annotation" withSubpackages="true" static="false" />
<emptyLine />
<package name="com.natpryce.makeiteasy" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.powermock" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.mockito" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<emptyLine />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
</value>
</option>
<option name="ENABLE_JAVADOC_FORMATTING" value="false" />
</JavaCodeStyleSettings>
<Properties>
<option name="KEEP_BLANK_LINES" value="true" />
</Properties>
<codeStyleSettings language="JAVA">
<option name="RIGHT_MARGIN" value="120" />
<option name="BLANK_LINES_BEFORE_PACKAGE" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="5" />
<option name="WRAP_ON_TYPING" value="0" />
<option name="SOFT_MARGINS" value="90" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -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 <http://www.gnu.org/licenses/>.
*/

View File

@ -0,0 +1,4 @@
#parse("File Header.java")
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
public @interface ${NAME} {
}

View File

@ -0,0 +1,4 @@
#parse("File Header.java")
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
class ${NAME} {
}

View File

@ -0,0 +1,4 @@
#parse("File Header.java")
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
public enum ${NAME} {
}

View File

@ -0,0 +1,4 @@
#parse("File Header.java")
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
public interface ${NAME} {
}

15
.travis.yml Normal file
View File

@ -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

14
CODEOWNERS Normal file
View File

@ -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

101
CONTRIBUTING.md Normal file
View File

@ -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)

661
LICENSE Normal file
View File

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 <http://www.gnu.org/licenses/>.
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
<http://www.gnu.org/licenses/>.

279
Makefile Normal file
View File

@ -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

20
README.md Normal file
View File

@ -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).

83
apitest/dao-setup.gradle Normal file
View File

@ -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')
}
}

6
apitest/docs/README.md Normal file
View File

@ -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.

View File

@ -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 Bisqs
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=<path>` option:
```
$ ./bisq-apitest --apiPassword=xyz \
--supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon \
--shutdownAfterTests=false \
--bitcoinPath=<bitcoin-core-home>/bin
```
If your bitcoin-core binaries are not statically linked to your BerkleyDB library, you can specify the path to it
with the `-berkeleyDbLibPath=<path>` option:
```
$ ./bisq-apitest --apiPassword=xyz \
--supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon \
--shutdownAfterTests=false \
--bitcoinPath=<bitcoin-core-home>/bin \
--berkeleyDbLibPath=<lib-berkleydb-path>
```
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-cores `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 scripts 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=<direction> The trade direciton, BUY or SELL.
-c=<country> The two letter country code, US, FR, AT, RU, etc.
-f=<fixed-price> The offers fixed price.
OR (-f and -m options mutually exclusive, use one or the other)
-m=<margin-from-price> The offers margin (%) from market price.
-a=<btc-amount> 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 Apis default server listening port is `9998`, and you do not need to specify a `port=<port>` option in a
CLI command unless you change the servers `apiPort=<listening-port>`. In the test harness, Alices Api port is
`9998`, Bobs 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 (Bobs or Alices) the CLI is sending requests to.
### CLI Help
Useful information can be found using the CLIs `--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=<wallet-password>
```
Set a new password on your already encrypted wallet:
```
$ ./bisq-cli --password=xyz setwalletpassword --wallet-password=<wallet-password> \
--new-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=<wallet-password> --timeout=<seconds>
```
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 Bobs balance (using port `9999`), not Alices balance._
Show Bobs 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=<btc-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 wallets 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=<bsq-address> --amount=<bsq-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=<bsq-address> --amount=<bsq-amount> --tx-fee-rate=10
```
Send BTC:
```
$ ./bisq-cli --password=xyz --port=9998 sendbtc --address=<btc-address> --amount=<btc-amount>
```
Send BTC with a withdrawal transaction fee of 20 sats/byte:
```
$ ./bisq-cli --password=xyz --port=9998 sendbtc --address=<btc-address> --amount=<btc-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 UIs 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=<sats/byte>` 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 Users 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 files 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=<BUY|SELL> --currency-code=<currency-code>
```
To look at a specific offer you created:
```
$ ./bisq-cli --password=xyz --port=9998 getmyoffer --offer-id=<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=<BUY|SELL> --currency-code=<currency-code>
```
To look at a specific, available offer you could take:
```
$ ./bisq-cli --password=xyz --port=9998 getoffer --offer-id=<offer-id>
```
### Removing An Offer
To cancel one of your offers:
```
$ ./bisq-cli --password=xyz --port=9998 canceloffer --offer-id=<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=<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=<trade-id> --show-contract=true
```
The `gettrade` commands 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 sellers BTC payout tx has been broadcast to the network.
Withdrawn YES if the buyers 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 takers 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 trades 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=<trade-id>`:
```
$ ./bisq-cli --password=xyz --port=9998 confirmpaymentstarted --trade-id=<trade-id>
$ ./bisq-cli --password=xyz --port=9999 confirmpaymentreceived --trade-id=<trade-id>
$ ./bisq-cli --password=xyz --port=9998 keepfunds --trade-id=<trade-id>
$ ./bisq-cli --password=xyz --port=9999 withdrawfunds --trade-id=<trade-id> --address=<btc-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`.

90
apitest/docs/build-run.md Normal file
View File

@ -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=<api-password> --apiPort=<api-port(default=9998)> --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)

View File

@ -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

View File

@ -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.

View File

@ -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)

15
apitest/scripts/get-bisq-pid.sh Executable file
View File

@ -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}'

View File

@ -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 <limit-price> -d <direction> -c <country-code> (-m <mkt-price-margin(%)> || -f <fixed-price>) -a <amount(btc)> [-w <price-poll-interval(s)>]
#
# 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

236
apitest/scripts/mainnet-test.sh Executable file
View File

@ -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] <method> [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] <method> [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" ]
}

View File

@ -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 <direction> -c <country-code> (-m <mkt-price-margin(%)> || -f <fixed-price>) -a <amount(btc)>
#
# 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

View File

@ -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 <country-code>] [-f <fixed-price> || -m <margin-from-price>] [-a <amount in btc>]" 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 <country-code>] [-f <fixed-price> || -m <margin-from-price>] [-a <amount in btc>] [-w <price-poll-interval(s)>]" 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 <price-poll-interval(s)> 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
}

View File

@ -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 <$?> <msg-prefix>
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"
}

View File

@ -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 <direction> -c <country-code> -m <mkt-price-margin(%)> - f <fixed-price> -a <amount(btc)>
#
# 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

View File

@ -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')

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<SetupTask.Status> bitcoindTaskFuture;
@Nullable
private SetupTask seedNodeTask;
@Nullable
private Future<SetupTask.Status> seedNodeTaskFuture;
@Nullable
private SetupTask arbNodeTask;
@Nullable
private Future<SetupTask.Status> arbNodeTaskFuture;
@Nullable
private SetupTask aliceNodeTask;
@Nullable
private Future<SetupTask.Status> aliceNodeTaskFuture;
@Nullable
private SetupTask bobNodeTask;
@Nullable
private Future<SetupTask.Status> 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<Throwable> 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<Throwable> shutDownAll(SetupTask[] orderedTasks) {
Optional<Throwable> 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<SetupTask.Status> 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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<SetupTask.Status> {
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 + "]";
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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());
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> 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<Void> helpOpt =
parser.accepts(HELP, "Print this help text")
.forHelp();
ArgumentAcceptingOptionSpec<String> 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<File> appDataDirOpt =
parser.accepts(ROOT_APP_DATA_DIR, "Application data directory")
.withRequiredArg()
.ofType(File.class)
.defaultsTo(new File(baseBuildResourcesDir));
ArgumentAcceptingOptionSpec<String> bashPathOpt =
parser.accepts(BASH_PATH, "Bash path")
.withRequiredArg()
.ofType(String.class)
.defaultsTo(
(getenv("SHELL") == null || !getenv("SHELL").contains("bash"))
? "/bin/bash"
: getenv("SHELL"));
ArgumentAcceptingOptionSpec<String> berkeleyDbLibPathOpt =
parser.accepts(BERKELEYDB_LIB_PATH, "Berkeley DB lib path")
.withRequiredArg()
.ofType(String.class).defaultsTo(EMPTY);
ArgumentAcceptingOptionSpec<String> bitcoinPathOpt =
parser.accepts(BITCOIN_PATH, "Bitcoin path")
.withRequiredArg()
.ofType(String.class).defaultsTo("/usr/local/bin");
ArgumentAcceptingOptionSpec<String> bitcoinRegtestHostOpt =
parser.accepts(BITCOIN_REGTEST_HOST, "Bitcoin Core regtest host")
.withRequiredArg()
.ofType(String.class).defaultsTo(InetAddress.getLoopbackAddress().getHostAddress());
ArgumentAcceptingOptionSpec<Integer> bitcoinRpcPortOpt =
parser.accepts(BITCOIN_RPC_PORT, "Bitcoin Core rpc port (non-default)")
.withRequiredArg()
.ofType(Integer.class).defaultsTo(19443);
ArgumentAcceptingOptionSpec<String> bitcoinRpcUserOpt =
parser.accepts(BITCOIN_RPC_USER, "Bitcoin rpc user")
.withRequiredArg()
.ofType(String.class).defaultsTo("apitest");
ArgumentAcceptingOptionSpec<String> bitcoinRpcPasswordOpt =
parser.accepts(BITCOIN_RPC_PASSWORD, "Bitcoin rpc password")
.withRequiredArg()
.ofType(String.class).defaultsTo("apitest");
ArgumentAcceptingOptionSpec<String> apiPasswordOpt =
parser.accepts(API_PASSWORD, "gRPC API password")
.withRequiredArg()
.defaultsTo("xyz");
ArgumentAcceptingOptionSpec<Boolean> runSubprojectJarsOpt =
parser.accepts(RUN_SUBPROJECT_JARS,
"Run subproject build jars instead of full build jars")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Long> 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<Boolean> skipTestsOpt =
parser.accepts(SKIP_TESTS,
"Start apps, but skip tests")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> shutdownAfterTestsOpt =
parser.accepts(SHUTDOWN_AFTER_TESTS,
"Terminate all processes after tests")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(true);
ArgumentAcceptingOptionSpec<String> 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<String> callRateMeteringConfigPathOpt =
parser.accepts(CALL_RATE_METERING_CONFIG_PATH,
"Install a ratemeters.json file to configure call rate metering interceptors")
.withRequiredArg()
.defaultsTo(EMPTY);
ArgumentAcceptingOptionSpec<Boolean> enableBisqDebuggingOpt =
parser.accepts(ENABLE_BISQ_DEBUGGING,
"Start Bisq apps with remote debug options")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> 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<OptionSet> 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<OptionSet> 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<OptionSet> 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<String> 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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 <a href="https://github.com/bisq-network/bisq/blob/master/docs/dev-setup.md">dev-setup.md</a>
@see <a href="https://github.com/bisq-network/bisq/blob/master/docs/dao-setup.md">dao-setup.md</a>
*/
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" +
'}';
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Throwable> startupExceptions;
protected final List<Throwable> 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<Throwable> exceptions, org.slf4j.Logger log) {
for (Throwable t : exceptions) {
log.error("", t);
}
}
@Override
public List<Throwable> getStartupExceptions() {
return startupExceptions;
}
@Override
public List<Throwable> 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");
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String>
public String getOutput() {
return this.output;
}
// TODO return Optional<String>
public String getError() {
return this.error;
}
@NotNull
private List<String> 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");
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> 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");
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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");
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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));
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Throwable> exceptions, org.slf4j.Logger log);
List<Throwable> getStartupExceptions();
List<Throwable> getShutdownExceptions();
void shutdown();
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 <http://www.gnu.org/licenses/>.
*
* Please ee the following page for the LGPL license:
* http://www.gnu.org/licenses/lgpl.txt
*
*/
@Slf4j
class SystemCommandExecutor {
private final List<String> cmdOptions;
private ThreadedStreamHandler inputStreamHandler;
private ThreadedStreamHandler errorStreamHandler;
public SystemCommandExecutor(final List<String> 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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 <http://www.gnu.org/licenses/>.
*
* 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;
}
}

View File

@ -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

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--
The :daemon & :cli jars contain their own logback.xml config files, which causes chatty logback startup.
To avoid chatty logback msgs during its configuration, pass logback.configurationFile as a system property:
-Dlogback.configurationFile=apitest/build/resources/main/logback.xml
The gradle build file takes care of adding this system property to the bisq-apitest script.
-->
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n)</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE_APPENDER"/>
</root>
<logger name="io.grpc.netty" level="WARN"/>
</configuration>

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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'.
* <p>
* 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
* <a href="https://github.com/bisq-network/bisq/blob/master/docs/dev-setup.md">dev-setup.md</a>
* and <a href="https://github.com/bisq-network/bisq/blob/master/docs/dao-setup.md">dao-setup.md</a>.
* <p>
* 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.
* <p>
* During a build, the
* <a href="https://github.com/bisq-network/bisq/blob/master/docs/dao-setup.zip">dao-setup.zip</a>
* 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).
* <p>
* Initial Alice balances & accounts: 10.0 BTC, 1000000.00 BSQ, USD PerfectMoney dummy
* <p>
* 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;
}
}

View File

@ -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())));
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Enum<?>[], 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));
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> 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<OfferInfo> 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());
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<OfferInfo> 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<OfferInfo> 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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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());
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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)), "%"));
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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());
}
}

View File

@ -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<String> 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<String, Object> 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<String, Object> emptyForm = (Map<String, Object>) 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<TradeCurrency> expectedTradeCurrencies,
PaymentAccount paymentAccount) {
assertNotNull(paymentAccount.getTradeCurrencies());
assertArrayEquals(expectedTradeCurrencies.toArray(), paymentAccount.getTradeCurrencies().toArray());
}
protected final void verifyUserPayloadHasPaymentAccountWithId(GrpcClient grpcClient,
String paymentAccountId) {
Optional<protobuf.PaymentAccount> 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<String, Object> 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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<TradeCurrency> 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<TradeCurrency> 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())));
}
}
}

View File

@ -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<String> paymentMethodIds = aliceClient.getPaymentMethods()
.stream()
.map(PaymentMethod::getId)
.collect(Collectors.toList());
assertTrue(paymentMethodIds.size() >= 20);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}

View File

@ -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<Integer> 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)));
}
}
}

View File

@ -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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<TradeInfo> 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<TradeInfo> 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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<TradeInfo> 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<TradeInfo> 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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<TradeInfo> 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<TradeInfo> 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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<TradeInfo> 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<TradeInfo> 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);
}
}
}

View File

@ -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));
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 <pid>
// 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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}

View File

@ -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."));
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<OfferInfo>
*/
public List<OfferInfo> 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<OfferInfo>
*/
public List<OfferInfo> getBuyOffers(String currencyCode) {
return grpcClient.getOffers("BUY", currencyCode);
}
/**
* Return SELL offers for the given currencyCode.
* @param currencyCode
* @return List<OfferInfo>
*/
public List<OfferInfo> 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<Exception, String> 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_]+: ", ""));
}
}

View File

@ -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<String, Object> 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<String, Object> 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<String, Object> getPaymentAccountFormMap(String paymentMethodId) {
PaymentAccountForm paymentAccountForm = new PaymentAccountForm();
File jsonFormTemplate = paymentAccountForm.getPaymentAccountForm(paymentMethodId);
jsonFormTemplate.deleteOnExit();
String jsonString = paymentAccountForm.toJsonString(jsonFormTemplate);
//noinspection unchecked
return (Map<String, Object>) gson.fromJson(jsonString, Object.class);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Long> nextAmount = () ->
this.getPaymentAccount().getPaymentMethod().getId().equals(F2F_ID)
? (long) (10000000 + RANDOM.nextInt(2500000))
: (long) (750000 + RANDOM.nextInt(250000));
@SuppressWarnings("FieldCanBeLocal")
private final Supplier<Long> 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<Double> 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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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");
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Long> randomDelay = () -> (long) (2000 + RANDOM.nextInt(5000));
protected final AtomicLong protocolStepStartTime = new AtomicLong(0);
protected final Consumer<ProtocolStep> 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<TradeInfo, TradeInfo> waitForTakerFeeTxConfirm = (trade) -> {
sleep(5000);
waitForTakerFeeTxPublished(trade.getTradeId());
waitForTakerFeeTxConfirmed(trade.getTradeId());
return trade;
};
protected final Function<TradeInfo, TradeInfo> 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<TradeInfo, TradeInfo> 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<TradeInfo, TradeInfo> 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<TradeInfo, TradeInfo> 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<TradeInfo, TradeInfo> 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<TradeInfo, TradeInfo> 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<TradeInfo> 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");
}
}

View File

@ -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<Supplier<OfferInfo>, TradeInfo> makeTrade = waitForNewTrade.andThen(waitForTakerFeeTxConfirm);
var trade = makeTrade.apply(randomOffer);
var makerIsBuyer = trade.getOffer().getDirection().equalsIgnoreCase(BUY);
Function<TradeInfo, TradeInfo> completeFiatTransaction = makerIsBuyer
? sendPaymentStartedMessage.andThen(waitForPaymentReceivedConfirmation)
: waitForPaymentStartedMessage.andThen(sendPaymentReceivedMessage);
completeFiatTransaction.apply(trade);
Function<TradeInfo, TradeInfo> closeTrade = waitForPayoutTx.andThen(keepFundsFromTrade);
closeTrade.apply(trade);
currentProtocolStep = DONE;
}
private final Supplier<OfferInfo> 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<Supplier<OfferInfo>, 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<TradeInfo> 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");
}
}

View File

@ -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
}

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