mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-11-16 07:47:42 +00:00
Bisq
This commit is contained in:
commit
8a38081c04
2800 changed files with 344130 additions and 0 deletions
26
.editorconfig
Normal file
26
.editorconfig
Normal 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
15
.gitattributes
vendored
Normal 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
45
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal 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
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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
18
.github/ISSUE_TEMPLATE/new_asset.md
vendored
Normal 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
15
.github/boring-cyborg.yml
vendored
Normal 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
35
.github/stale.yml
vendored
Normal 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
37
.gitignore
vendored
Normal 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*
|
192
.idea/codeStyles/Project.xml
Normal file
192
.idea/codeStyles/Project.xml
Normal 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>
|
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
16
.idea/fileTemplates/includes/File Header.java
Normal file
16
.idea/fileTemplates/includes/File Header.java
Normal 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/>.
|
||||
*/
|
4
.idea/fileTemplates/internal/AnnotationType.java
Normal file
4
.idea/fileTemplates/internal/AnnotationType.java
Normal file
|
@ -0,0 +1,4 @@
|
|||
#parse("File Header.java")
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
public @interface ${NAME} {
|
||||
}
|
4
.idea/fileTemplates/internal/Class.java
Normal file
4
.idea/fileTemplates/internal/Class.java
Normal file
|
@ -0,0 +1,4 @@
|
|||
#parse("File Header.java")
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
class ${NAME} {
|
||||
}
|
4
.idea/fileTemplates/internal/Enum.java
Normal file
4
.idea/fileTemplates/internal/Enum.java
Normal file
|
@ -0,0 +1,4 @@
|
|||
#parse("File Header.java")
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
public enum ${NAME} {
|
||||
}
|
4
.idea/fileTemplates/internal/Interface.java
Normal file
4
.idea/fileTemplates/internal/Interface.java
Normal 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
15
.travis.yml
Normal 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
14
CODEOWNERS
Normal 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
101
CONTRIBUTING.md
Normal 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
661
LICENSE
Normal 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
279
Makefile
Normal 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
20
README.md
Normal 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
83
apitest/dao-setup.gradle
Normal 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
6
apitest/docs/README.md
Normal 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.
|
499
apitest/docs/api-beta-test-guide.md
Normal file
499
apitest/docs/api-beta-test-guide.md
Normal 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 Bisq’s
|
||||
pre-configured DAO / dev / regtest setup files.
|
||||
```
|
||||
$ cd api-beta-test
|
||||
$ ./gradlew clean build :apitest:installDaoSetup -x test # if you want to skip Bisq tests
|
||||
$ ./gradlew clean build :apitest:installDaoSetup # if you want to run Bisq tests
|
||||
```
|
||||
|
||||
## Running Api Test Harness
|
||||
|
||||
If your bitcoin-core binaries are in your system `PATH`, start bitcoind in regtest-mode, Bisq seednode and arbitration
|
||||
node daemons, plus Bob & Alice daemons in a bash terminal with the following bash command:
|
||||
```
|
||||
$ ./bisq-apitest --apiPassword=xyz \
|
||||
--supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon \
|
||||
--shutdownAfterTests=false
|
||||
```
|
||||
|
||||
If your bitcoin-core binaries are not in your system `PATH`, you can specify the bitcoin-core bin directory with the
|
||||
`-–bitcoinPath=<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-core’s `bitcoin-cli` binary must be in the system
|
||||
`PATH`. (The script generates regtest blocks with it.)
|
||||
|
||||
### Description
|
||||
|
||||
The regtest trade simulation script `apitest/scripts/trade-simulation.sh` is a useful introduction to the Bisq Api.
|
||||
The bash script’s output is intended to serve as a tutorial, showing how the CLI can be used to create payment
|
||||
accounts for Bob and Alice, create an offer, take the offer, and complete a trade.
|
||||
(The bash script itself is not intended to be as useful as the output.) The output is generated too quickly to
|
||||
follow in real time, so let the script complete before studying the output from start to finish.
|
||||
|
||||
The script takes four options:
|
||||
```
|
||||
-d=<direction> The trade direciton, BUY or SELL.
|
||||
-c=<country> The two letter country code, US, FR, AT, RU, etc.
|
||||
-f=<fixed-price> The offer’s fixed price.
|
||||
OR (-f and -m options mutually exclusive, use one or the other)
|
||||
-m=<margin-from-price> The offer’s 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 Api’s 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 server’s `–apiPort=<listening-port>`. In the test harness, Alice’s Api port is
|
||||
`9998`, Bob’s is `9999`. When you manually test the Api using the test harness, be aware of the port numbers being
|
||||
used in the CLI commands, so you know which server (Bob’s or Alice’s) the CLI is sending requests to.
|
||||
|
||||
### CLI Help
|
||||
|
||||
Useful information can be found using the CLI’s `--help` option.
|
||||
|
||||
For list of supported CLI commands:
|
||||
```
|
||||
$ ./bisq-cli --help (the –password option is not needed because there is no server request)
|
||||
```
|
||||
|
||||
For help with a specific CLI command:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --help getbalance
|
||||
OR
|
||||
$ ./bisq-cli --password=xyz getbalance --help
|
||||
```
|
||||
The position of `--help` option does not matter. If a supported positional command option is present,
|
||||
method help will be returned from the server. Also note an api password is required to get help from the server.
|
||||
|
||||
### Working With Encrypted Wallet
|
||||
|
||||
There is no need to secure your regtest Bisq wallet with an encryption password when running these examples,
|
||||
but you should encrypt your mainnet wallet as you probably already do when using the Bisq UI to transact in
|
||||
real BTC. This section explains how to encrypt your Bisq wallet with the CLI, and unlock it before performing wallet
|
||||
related operations such as creating and taking offers, checking balances, and sending BSQ and BTC to external wallets.
|
||||
|
||||
Encrypt your wallet with a password:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz setwalletpassword --wallet-password=<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 Bob’s balance (using port `9999`), not Alice’s balance._
|
||||
|
||||
Show Bob’s full BTC wallet balance information:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9999 getbalance --currency-code=btc
|
||||
```
|
||||
|
||||
### Funding a Bisq Wallet
|
||||
|
||||
#### Receiving BTC
|
||||
|
||||
To receive BTC from an external wallet, find an unused BTC address (with a zero balance) to receive the BTC.
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getfundingaddresses
|
||||
```
|
||||
You can check a block explorer for the status of a transaction, or you can check your Bisq BTC wallet address directly:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getaddressbalance --address=<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 wallet’s balance to determine if the new funds have arrived:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9999 getbalance --currency-code=bsq
|
||||
```
|
||||
|
||||
### Sending BSQ and BTC to External Wallets
|
||||
|
||||
Below are commands for sending BSQ and BTC to external wallets.
|
||||
|
||||
Send BSQ:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 sendbsq --address=<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 UI’s setting view. The Api uses these same
|
||||
withdrawal transaction fee rates, and affords a third – as mentioned in the previous section -- withdrawal
|
||||
transaction fee option in the `sendbsq` and `sendbtc` commands. The `sendbsq` and `sendbtc` commands'
|
||||
`--tx-fee-rate=<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 User’s Custom Transaction Fee Preference
|
||||
To remove a custom withdrawal transaction fee rate preference, and revert to the network fee rate:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz unsettxfeerate
|
||||
```
|
||||
|
||||
### Creating Test Payment Accounts
|
||||
|
||||
Creating a payment account using the Api involves three steps:
|
||||
|
||||
1. Find the payment-method-id for the payment account type you wish to create. For example, if you want to
|
||||
create a face-to-face type payment account, find the face-to-face payment-method-id (`F2F`):
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getpaymentmethods
|
||||
```
|
||||
|
||||
2. Use the payment-method-id `F2F` found in the `getpaymentmethods` command output to create a blank payment account
|
||||
form:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getpaymentacctform --payment-method-id=F2F
|
||||
```
|
||||
This `getpaymentacctform` command generates a json file (form) for creating an `F2F` payment account,
|
||||
prints the file’s contents, and tells you where it is. In this example, the sever created an `F2F` account
|
||||
form named `f2f_1612381824625.json`.
|
||||
|
||||
|
||||
3. Manually edit the json file, and use its path in the `createpaymentacct` command.
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 createpaymentacct \
|
||||
--payment-account-form=f2f_1612381824625.json
|
||||
```
|
||||
_Note: You can rename the file before passing it to the `createpaymentacct` command._
|
||||
|
||||
The server will create and save the new payment account from details defined in the json file then
|
||||
return the new payment account to the CLI. The CLI will display the account ID with other details
|
||||
in the console, but if you ever need to find a payment account ID, use the `getpaymentaccts` command:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getpaymentaccts
|
||||
```
|
||||
|
||||
### Creating Offers
|
||||
|
||||
The createoffer command is the Api's most complex command (so far), but CLI posix-style options are self-explanatory,
|
||||
and CLI `createoffer` command help gives you specific information about each option.
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 createoffer --help
|
||||
```
|
||||
|
||||
#### Examples
|
||||
|
||||
The `trade-simulation.sh` script described above is an easy way to figure out how to use this command.
|
||||
In a previous example, Alice created a BUY/ EUR offer to buy 0.125 BTC at a fixed price of 30,800 EUR,
|
||||
and pay the Bisq maker fee in BSQ. Alice had already created an EUR face-to-face payment account with id
|
||||
`f3c1ec8b-9761-458d-b13d-9039c6892413`, and used this `createoffer` command:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 createoffer \
|
||||
--payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \
|
||||
--direction=BUY \
|
||||
--currency-code=EUR \
|
||||
--amount=0.125 \
|
||||
--fixed-price=30800 \
|
||||
--security-deposit=15.0 \
|
||||
--fee-currency=BSQ
|
||||
```
|
||||
|
||||
If Alice was in Japan, and wanted to create an offer to sell 0.125 BTC at 0.5% above the current market JPY price,
|
||||
putting up a 15% security deposit, the `createoffer` command to do that would be:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 createoffer \
|
||||
--payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \
|
||||
--direction=SELL \
|
||||
--currency-code=JPY \
|
||||
--amount=0.125 \
|
||||
--market-price-margin=0.5 \
|
||||
--security-deposit=15.0 \
|
||||
--fee-currency=BSQ
|
||||
```
|
||||
|
||||
The `trade-simulation.sh` script options that would generate the previous `createoffer` example is:
|
||||
```
|
||||
$ apitest/scripts/trade-simulation.sh -d sell -c jp -m 0.5 -a 0.125
|
||||
```
|
||||
|
||||
### Browsing Your Own Offers
|
||||
|
||||
There are different commands to browse available offers you can take, and offers you created.
|
||||
|
||||
To see all offers you created with a specific direction (BUY|SELL) and currency (CAD|EUR|USD|...):
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getmyoffers --direction=<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` command’s output shows the state of the trade from initial preparation through completion and closure.
|
||||
Output columns include:
|
||||
```
|
||||
Deposit Published YES if the taker fee tx deposit has been broadcast to the network.
|
||||
Deposit Confirmed YES if the taker fee tx deposit has been confirmed by the network.
|
||||
Fiat Sent YES if the buyer has sent a “payment started” message to seller.
|
||||
Fiat Received YES if the seller has sent a “payment received” message to buyer.
|
||||
Payout Published YES if the seller’s BTC payout tx has been broadcast to the network.
|
||||
Withdrawn YES if the buyer’s BTC proceeds have been sent to an external wallet.
|
||||
```
|
||||
Trade status information informs both sides of a trade which steps have been completed, and which step to perform next.
|
||||
It should be frequently checked by both sides before proceeding to the next step of the protocol.
|
||||
|
||||
_Note: There is some delay after a new trade is created due to the time it takes for a taker’s trade deposit fee
|
||||
transaction to be published and confirmed on the bitcoin network. Both sides of the trade can check the `gettrade`
|
||||
output's `Deposit Published` and `Deposit Confirmed` columns to find out when this early phase of the trade protocol is
|
||||
complete._
|
||||
|
||||
Once the taker fee transaction has been confirmed, payment can be sent, payment receipt confirmed, and the trade
|
||||
protocol completed. There are three CLI commands that must be performed in coordinated order by each side of the trade:
|
||||
```
|
||||
confirmpaymentstarted Buyer sends seller a message confirming payment has been sent.
|
||||
confirmpaymentreceived Seller sends buyer a message confirming payment has been received.
|
||||
keepfunds Keep trade proceeds in their Bisq wallets.
|
||||
OR
|
||||
withdrawfunds Send trade proceeds to an external wallet.
|
||||
```
|
||||
The last two mutually exclusive commands (`keepfunds` or `withdrawfunds`) may seem unnecessary, but they are critical
|
||||
because they inform the Bisq node that a trade’s state can be set to `CLOSED`. Please close out your trades with one
|
||||
or the other command.
|
||||
|
||||
Each of the CLI commands above takes one argument: `--trade-id=<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
90
apitest/docs/build-run.md
Normal 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)
|
||||
|
12
apitest/docs/regtest-port-conflicts.md
Normal file
12
apitest/docs/regtest-port-conflicts.md
Normal 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
|
35
apitest/docs/test-categories.md
Normal file
35
apitest/docs/test-categories.md
Normal 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.
|
||||
|
28
apitest/scripts/editf2faccountform.py
Normal file
28
apitest/scripts/editf2faccountform.py
Normal 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
15
apitest/scripts/get-bisq-pid.sh
Executable 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}'
|
159
apitest/scripts/limit-order-simulation.sh
Executable file
159
apitest/scripts/limit-order-simulation.sh
Executable 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
236
apitest/scripts/mainnet-test.sh
Executable 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" ]
|
||||
}
|
119
apitest/scripts/rolling-offer-simulation.sh
Executable file
119
apitest/scripts/rolling-offer-simulation.sh
Executable 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
|
312
apitest/scripts/trade-simulation-env.sh
Executable file
312
apitest/scripts/trade-simulation-env.sh
Executable 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
|
||||
}
|
571
apitest/scripts/trade-simulation-utils.sh
Executable file
571
apitest/scripts/trade-simulation-utils.sh
Executable 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"
|
||||
}
|
126
apitest/scripts/trade-simulation.sh
Executable file
126
apitest/scripts/trade-simulation.sh
Executable 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
|
5
apitest/scripts/version-parser.bash
Executable file
5
apitest/scripts/version-parser.bash
Executable 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')
|
80
apitest/src/main/java/bisq/apitest/ApiTestMain.java
Normal file
80
apitest/src/main/java/bisq/apitest/ApiTestMain.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
469
apitest/src/main/java/bisq/apitest/Scaffold.java
Normal file
469
apitest/src/main/java/bisq/apitest/Scaffold.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
85
apitest/src/main/java/bisq/apitest/SetupTask.java
Normal file
85
apitest/src/main/java/bisq/apitest/SetupTask.java
Normal 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 + "]";
|
||||
}
|
||||
}
|
||||
}
|
51
apitest/src/main/java/bisq/apitest/SmokeTestBashCommand.java
Normal file
51
apitest/src/main/java/bisq/apitest/SmokeTestBashCommand.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
72
apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java
Normal file
72
apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java
Normal 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());
|
||||
}
|
||||
}
|
380
apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java
Normal file
380
apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java
Normal 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);
|
||||
}
|
||||
}
|
133
apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java
Normal file
133
apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java
Normal 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" +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
156
apitest/src/main/java/bisq/apitest/linux/BashCommand.java
Normal file
156
apitest/src/main/java/bisq/apitest/linux/BashCommand.java
Normal 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");
|
||||
}
|
||||
}
|
266
apitest/src/main/java/bisq/apitest/linux/BisqProcess.java
Normal file
266
apitest/src/main/java/bisq/apitest/linux/BisqProcess.java
Normal 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");
|
||||
}
|
||||
}
|
182
apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java
Normal file
182
apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java
Normal 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");
|
||||
}
|
||||
}
|
117
apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java
Normal file
117
apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
42
apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java
Normal file
42
apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java
Normal 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();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
0
apitest/src/main/resources/apitest.properties
Normal file
0
apitest/src/main/resources/apitest.properties
Normal file
20
apitest/src/main/resources/blocknotify
Executable file
20
apitest/src/main/resources/blocknotify
Executable 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
|
20
apitest/src/main/resources/logback.xml
Normal file
20
apitest/src/main/resources/logback.xml
Normal 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>
|
184
apitest/src/test/java/bisq/apitest/ApiTestCase.java
Normal file
184
apitest/src/test/java/bisq/apitest/ApiTestCase.java
Normal 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;
|
||||
}
|
||||
}
|
58
apitest/src/test/java/bisq/apitest/JUnitHelper.java
Normal file
58
apitest/src/test/java/bisq/apitest/JUnitHelper.java
Normal 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())));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
149
apitest/src/test/java/bisq/apitest/method/MethodTest.java
Normal file
149
apitest/src/test/java/bisq/apitest/method/MethodTest.java
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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)), "%"));
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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())));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
88
apitest/src/test/java/bisq/apitest/scenario/OfferTest.java
Normal file
88
apitest/src/test/java/bisq/apitest/scenario/OfferTest.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
121
apitest/src/test/java/bisq/apitest/scenario/ScriptedBotTest.java
Normal file
121
apitest/src/test/java/bisq/apitest/scenario/ScriptedBotTest.java
Normal 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();
|
||||
}
|
||||
}
|
115
apitest/src/test/java/bisq/apitest/scenario/StartupTest.java
Normal file
115
apitest/src/test/java/bisq/apitest/scenario/StartupTest.java
Normal 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();
|
||||
}
|
||||
}
|
88
apitest/src/test/java/bisq/apitest/scenario/TradeTest.java
Normal file
88
apitest/src/test/java/bisq/apitest/scenario/TradeTest.java
Normal 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);
|
||||
}
|
||||
}
|
116
apitest/src/test/java/bisq/apitest/scenario/WalletTest.java
Normal file
116
apitest/src/test/java/bisq/apitest/scenario/WalletTest.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
77
apitest/src/test/java/bisq/apitest/scenario/bot/Bot.java
Normal file
77
apitest/src/test/java/bisq/apitest/scenario/bot/Bot.java
Normal 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."));
|
||||
}
|
||||
}
|
339
apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java
Normal file
339
apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java
Normal 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_]+: ", ""));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
177
apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java
Normal file
177
apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java
Normal 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);
|
||||
}
|
||||
}
|
141
apitest/src/test/java/bisq/apitest/scenario/bot/RobotBob.java
Normal file
141
apitest/src/test/java/bisq/apitest/scenario/bot/RobotBob.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue