Merge branch 'release/v1.6.0' into implement-segwit-for-bsq

This commit is contained in:
Steven Barclay 2021-03-20 23:18:49 +00:00 committed by GitHub
commit f21379160b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
379 changed files with 18082 additions and 4559 deletions

1
.gitignore vendored
View file

@ -34,3 +34,4 @@ deploy
/monitor/monitor-tor/*
.java-version
.localnet
/apitest/src/main/resources/dao-setup*

View file

@ -89,11 +89,11 @@
# 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+N
# 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+D and follow the same
# 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

View file

@ -3,3 +3,4 @@
- [build-run.md](build-run.md): Build and run API tests at the command line and from Intellij.
- [test-categories.md](test-categories.md): How to categorize a test case as `method`, `scenario` or `e2e`.
- [regtest-port-conflicts.md](regtest-port-conflicts.md): Avoid port conflicts when running multiple bitcoin-core apps in regtest mode.
- [api-beta-test-guide.md](api-beta-test-guide.md): How to run the test harness and tutorial script, and beta test the API with the new CLI.

View file

@ -0,0 +1,518 @@
# 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 or 0.20
**Git Client**
## Clone and Build Source Code
Beta testing can be done with no knowledge of how git works, but you need a git client to get the source code.
Clone the Bisq master branch into a project folder of your choice. In this document, the root project folder is
called `api-beta-test`.
```
$ git clone https://github.com/bisq-network/bisq.git api-beta-test
```
Change your current working directory to `api-beta-test`, build the source, and download / install Bisqs
pre-configured DAO / dev / regtest setup files.
```
$ cd api-beta-test
$ ./gradlew clean build :apitest:installDaoSetup -x test # if you want to skip Bisq tests
$ ./gradlew clean build :apitest:installDaoSetup # if you want to run Bisq tests
```
## Running Api Test Harness
If your bitcoin-core binaries are in your system `PATH`, start bitcoind in regtest-mode, Bisq seednode and arbitration
node daemons, plus Bob & Alice daemons in a bash terminal with the following bash command:
```
$ ./bisq-apitest --apiPassword=xyz \
--supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon \
--shutdownAfterTests=false
```
If your bitcoin-core binaries are not in your system `PATH`, you can specify the bitcoin-core bin directory with the
`-bitcoinPath=<path>` option:
```
$ ./bisq-apitest --apiPassword=xyz \
--supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon \
--shutdownAfterTests=false \
--bitcoinPath=<bitcoin-core-home>/bin
```
If your bitcoin-core binaries are not statically linked to your BerkleyDB library, you can specify the path to it
with the `-berkeleyDbLibPath=<path>` option:
```
$ ./bisq-apitest --apiPassword=xyz \
--supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon \
--shutdownAfterTests=false \
--bitcoinPath=<bitcoin-core-home>/bin \
--berkeleyDbLibPath=<lib-berkleydb-path>
```
Alternatively, you can specify any or all of these bisq-apitest options in a properties file located in
`apitest/src/main/resources/apitest.properties`.
In this example, a beta tester uses the `apitest.properties` below, instead of `bisq-cli` options.
```
supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon
apiPassword=xyz
shutdownAfterTests=false
bitcoinPath=/home/beta-tester/path-to-my-bitcoin-core/bin
```
Start up the test harness with without command options:
```
$ ./bisq-apitest
```
If you edit `apitest.properties`, do not forget to re-build the source. You do not need to do a full clean and
build, or run tests. The following build command should finish quickly.
```
$ ./gradlew build :apitest:installDaoSetup -x test
```
You should see the test harness startup bitcoin-core and other Bisq daemons in your console, run a
`bitcoin-cli getwalletinfo` command, and generate a regtest btc block.
After the test harness tells you how to shut it down by entering `^C`, the test harness is ready to use.
## Running Trade Simulation Script
_Warning: again, it is assumed the beta tester has a reasonably fast machine, or the scripted wait times -- for
the other side to perform his step in the protocol, and for btc block generation and asynchronous processing
of new btc blocks by test daemons -- may not be long enough._
### System Requirements
Same as described at the top of this document, but your bitcoin-cores `bitcoin-cli` binary must be in the system
`PATH`. (The script generates regtest blocks with it.)
### Description
The regtest trade simulation script `apitest/scripts/trade-simulation.sh` is a useful introduction to the Bisq Api.
The bash scripts output is intended to serve as a tutorial, showing how the CLI can be used to create payment
accounts for Bob and Alice, create an offer, take the offer, and complete a trade.
(The bash script itself is not intended to be as useful as the output.) The output is generated too quickly to
follow in real time, so let the script complete before studying the output from start to finish.
The script takes four options:
```
-d=<direction> The trade direciton, BUY or SELL.
-c=<country> The two letter country code, US, FR, AT, RU, etc.
-f=<fixed-price> The offers fixed price.
OR (-f and -m options mutually exclusive, use one or the other)
-m=<margin-from-price> The offers margin (%) from market price.
-a=<btc-amount> The amount of btc to buy or sell.
```
### Examples
This simulation creates US / USD face-to-face payment accounts for Bob and Alice. Alice (always the trade maker)
creates a SELL / USD offer for the amount of 0.1 BTC, at a price 2% below the current market price.
Bob (always the taker), will use his face-to-face account to take the offer, then the two sides will complete
the trade, checking their trade status along the way, and their BSQ / BTC balances when the trade is closed.
```
$ apitest/scripts/trade-simulation.sh -d sell -c us -m 2.00 -a 0.1
```
In the next example, Bob and Alice create Austrian face-to-face payment accounts. Alice creates a BUY/ EUR
offer to buy 0.125 BTC at a fixed price of 30,800 EUR.
```
$ apitest/scripts/trade-simulation.sh -d buy -c at -f 30800 -a 0.125
```
## Manual Testing
The test harness used by the simulation script described in the previous section can also be used for manual CLI
testing, and you can leave it running as you try the commands described below.
The Apis default server listening port is `9998`, and you do not need to specify a `port=<port>` option in a
CLI command unless you change the servers `apiPort=<listening-port>`. In the test harness, Alices Api port is
`9998`, Bobs is `9999`. When you manually test the Api using the test harness, be aware of the port numbers being
used in the CLI commands, so you know which server (Bobs or Alices) the CLI is sending requests to.
### Registering Test Dispute Agents
If you ran the `trade-simulation.sh` script in your currently running test harness, dispute agents have
already been registered in the arbitration node, and you can run any of the commands described in the following
sections.
If you have not run the `trade-simulation.sh` script against the test harness, you will need to
manually register dispute agents in the arbitration node before you can initiate a trade. Copy, paste and run
the following CLI commands to register a `mediator` and a `refundagent`. Do not change the commands' port `9997`
option (the test arbitration node's listening port).
```
$ ./bisq-cli --password=xyz --port=9997 registerdisputeagent --dispute-agent-type=mediator \
--registration-key=6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a
$ ./bisq-cli --password=xyz --port=9997 registerdisputeagent --dispute-agent-type=refundagent \
--registration-key=6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a
```
_Note: The API cannot be used to register dispute agents on nodes connected to `mainnet`._
### CLI Help
Useful information can be found using the CLIs `--help` option.
For list of supported CLI commands:
```
$ ./bisq-cli --help (the password option is not needed because there is no server request)
```
For help with a specific CLI command:
```
$ ./bisq-cli --password=xyz --help getbalance
OR
$ ./bisq-cli --password=xyz getbalance --help
```
The position of `--help` option does not matter. If a supported positional command option is present,
method help will be returned from the server. Also note an api password is required to get help from the server.
### Working With Encrypted Wallet
There is no need to secure your regtest Bisq wallet with an encryption password when running these examples,
but you should encrypt your mainnet wallet as you probably already do when using the Bisq UI to transact in
real BTC. This section explains how to encrypt your Bisq wallet with the CLI, and unlock it before performing wallet
related operations such as creating and taking offers, checking balances, and sending BSQ and BTC to external wallets.
Encrypt your wallet with a password:
```
$ ./bisq-cli --password=xyz setwalletpassword --wallet-password=<wallet-password>
```
Set a new password on your already encrypted wallet:
```
$ ./bisq-cli --password=xyz setwalletpassword --wallet-password=<wallet-password> \
--new-wallet-password=<new-wallet-password>
```
Unlock your password encrypted wallet for N seconds before performing sensitive wallet operations:
```
$ ./bisq-cli --password=xyz unlockwallet --wallet-password=<wallet-password> --timeout=<seconds>
```
You can override a `timeout` before it expires by calling `unlockwallet` again.
Lock your wallet before the `unlockwallet` timeout expires:
```
$ ./bisq-cli --password=xyz lockwallet
```
### Checking Balances
Show full BSQ and BTC wallet balance information:
```
$ ./bisq-cli --password=xyz --port=9998 getbalance
```
Show full BSQ wallet balance information:
```
$ ./bisq-cli --password=xyz --port=9999 getbalance --currency-code=bsq
```
_Note: The example above is asking for Bobs balance (using port `9999`), not Alices balance._
Show Bobs full BTC wallet balance information:
```
$ ./bisq-cli --password=xyz --port=9999 getbalance --currency-code=btc
```
### Funding a Bisq Wallet
#### Receiving BTC
To receive BTC from an external wallet, find an unused BTC address (with a zero balance) to receive the BTC.
```
$ ./bisq-cli --password=xyz --port=9998 getfundingaddresses
```
You can check a block explorer for the status of a transaction, or you can check your Bisq BTC wallet address directly:
```
$ ./bisq-cli --password=xyz --port=9998 getaddressbalance --address=<btc-address>
```
#### Receiving BSQ
To receive BSQ from an external wallet, find an unused BSQ address:
```
$ ./bisq-cli --password=xyz --port=9998 getunusedbsqaddress
```
Give the public address to the sender. After the BSQ is sent, you can check block explorers for the status of
the transaction. There is no support (yet) to check the balance of an individual BSQ address in your wallet,
but you can check your BSQ wallets balance to determine if the new funds have arrived:
```
$ ./bisq-cli --password=xyz --port=9999 getbalance --currency-code=bsq
```
### Sending BSQ and BTC to External Wallets
Below are commands for sending BSQ and BTC to external wallets.
Send BSQ:
```
$ ./bisq-cli --password=xyz --port=9998 sendbsq --address=<bsq-address> --amount=<bsq-amount>
```
_Note: Sending BSQ to non-Bisq wallets is not supported and highly discouraged._
Send BSQ with a withdrawal transaction fee of 10 sats/byte:
```
$ ./bisq-cli --password=xyz --port=9998 sendbsq --address=<bsq-address> --amount=<bsq-amount> --tx-fee-rate=10
```
Send BTC:
```
$ ./bisq-cli --password=xyz --port=9998 sendbtc --address=<btc-address> --amount=<btc-amount>
```
Send BTC with a withdrawal transaction fee of 20 sats/byte:
```
$ ./bisq-cli --password=xyz --port=9998 sendbtc --address=<btc-address> --amount=<btc-amount> --tx-fee-rate=20
```
### Withdrawal Transaction Fees
If you have traded using the Bisq UI, you are probably aware of the default network bitcoin withdrawal transaction
fee and custom withdrawal transaction fee user preference in the UIs setting view. The Api uses these same
withdrawal transaction fee rates, and affords a third as mentioned in the previous section -- withdrawal
transaction fee option in the `sendbsq` and `sendbtc` commands. The `sendbsq` and `sendbtc` commands'
`--tx-fee-rate=<sats/byte>` options override both the default network fee rate, and your custom transaction fee
setting for the execution of those commands.
#### Using Default Network Transaction Fee
If you have not set your custom withdrawal transaction fee setting, the default network transaction fee will be used
when withdrawing funds. In either case, you can check the current (default or custom) withdrawal transaction fee rate:
```
$ ./bisq-cli --password=xyz gettxfeerate
```
#### Setting Custom Transaction Fee Preference
To set a custom withdrawal transaction fee rate preference of 50 sats/byte:
```
$ ./bisq-cli --password=xyz settxfeerate --tx-fee-rate=50
```
#### Removing Users Custom Transaction Fee Preference
To remove a custom withdrawal transaction fee rate preference, and revert to the network fee rate:
```
$ ./bisq-cli --password=xyz unsettxfeerate
```
### Creating Test Payment Accounts
Creating a payment account using the Api involves three steps:
1. Find the payment-method-id for the payment account type you wish to create. For example, if you want to
create a face-to-face type payment account, find the face-to-face payment-method-id (`F2F`):
```
$ ./bisq-cli --password=xyz --port=9998 getpaymentmethods
```
2. Use the payment-method-id `F2F` found in the `getpaymentmethods` command output to create a blank payment account
form:
```
$ ./bisq-cli --password=xyz --port=9998 getpaymentacctform --payment-method-id=F2F
```
This `getpaymentacctform` command generates a json file (form) for creating an `F2F` payment account,
prints the files contents, and tells you where it is. In this example, the sever created an `F2F` account
form named `f2f_1612381824625.json`.
3. Manually edit the json file, and use its path in the `createpaymentacct` command.
```
$ ./bisq-cli --password=xyz --port=9998 createpaymentacct \
--payment-account-form=f2f_1612381824625.json
```
_Note: You can rename the file before passing it to the `createpaymentacct` command._
The server will create and save the new payment account from details defined in the json file then
return the new payment account to the CLI. The CLI will display the account ID with other details
in the console, but if you ever need to find a payment account ID, use the `getpaymentaccts` command:
```
$ ./bisq-cli --password=xyz --port=9998 getpaymentaccts
```
### Creating Offers
The createoffer command is the Api's most complex command (so far), but CLI posix-style options are self-explanatory,
and CLI `createoffer` command help gives you specific information about each option.
```
$ ./bisq-cli --password=xyz --port=9998 createoffer --help
```
#### Examples
The `trade-simulation.sh` script described above is an easy way to figure out how to use this command.
In a previous example, Alice created a BUY/ EUR offer to buy 0.125 BTC at a fixed price of 30,800 EUR,
and pay the Bisq maker fee in BSQ. Alice had already created an EUR face-to-face payment account with id
`f3c1ec8b-9761-458d-b13d-9039c6892413`, and used this `createoffer` command:
```
$ ./bisq-cli --password=xyz --port=9998 createoffer \
--payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \
--direction=BUY \
--currency-code=EUR \
--amount=0.125 \
--fixed-price=30800 \
--security-deposit=15.0 \
--fee-currency=BSQ
```
If Alice was in Japan, and wanted to create an offer to sell 0.125 BTC at 0.5% above the current market JPY price,
putting up a 15% security deposit, the `createoffer` command to do that would be:
```
$ ./bisq-cli --password=xyz --port=9998 createoffer \
--payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \
--direction=SELL \
--currency-code=JPY \
--amount=0.125 \
--market-price-margin=0.5 \
--security-deposit=15.0 \
--fee-currency=BSQ
```
The `trade-simulation.sh` script options that would generate the previous `createoffer` example is:
```
$ apitest/scripts/trade-simulation.sh -d sell -c jp -m 0.5 -a 0.125
```
### Browsing Your Own Offers
There are different commands to browse available offers you can take, and offers you created.
To see all offers you created with a specific direction (BUY|SELL) and currency (CAD|EUR|USD|...):
```
$ ./bisq-cli --password=xyz --port=9998 getmyoffers --direction=<BUY|SELL> --currency-code=<currency-code>
```
To look at a specific offer you created:
```
$ ./bisq-cli --password=xyz --port=9998 getmyoffer --offer-id=<offer-id>
```
### Browsing Available Offers
To see all available offers you can take, with a specific direction (BUY|SELL) and currency (CAD|EUR|USD|...):
```
$ ./bisq-cli --password=xyz --port=9998 getoffers --direction=<BUY|SELL> --currency-code=<currency-code>
```
To look at a specific, available offer you could take:
```
$ ./bisq-cli --password=xyz --port=9998 getoffer --offer-id=<offer-id>
```
### Removing An Offer
To cancel one of your offers:
```
$ ./bisq-cli --password=xyz --port=9998 canceloffer --offer-id=<offer-id>
```
The offer will be removed from other Bisq users' offer views, and paid transaction fees will be forfeited.
### Editing an Existing Offer
Editing existing offers is not yet supported. You can cancel and re-create an offer, but paid transaction fees
for the canceled offer will be forfeited.
### Taking Offers
Taking an available offer involves two CLI commands: `getoffers` and `takeoffer`.
A CLI user browses available offers with the getoffers command. For example, the user browses SELL / EUR offers:
```
$ ./bisq-cli --password=xyz --port=9998 getoffers --direction=SELL --currency-code=EUR
```
And takes one of the available offers with an EUR payment account ( id `fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e`)
with the `takeoffer` command:
```
$ ./bisq-cli --password=xyz --port=9998 takeoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--payment-account=fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e \
--fee-currency=btc
```
The taken offer will be used to create a trade contract. The next section describes how to use the Api to execute
the trade.
### Completing Trade Protocol
The first step in the Bisq trade protocol is completed when a `takeoffer` command successfully creates a new trade from
the taken offer. After the Bisq nodes prepare the trade, its status can be viewed with the `gettrade` command:
```
$ ./bisq-cli --password=xyz --port=9998 gettrade --trade-id=<trade-id>
```
The `trade-id` is the same as the taken `offer-id`, but when viewing and interacting with trades, it is referred to as
the `trade-id`. Note that the `trade-id` argument is a full `offer-id`, not a truncated `short-id` as displayed in the
Bisq UI.
You can also view the entire trade contract in `json` format by using the `gettrade` command's `--show-contract=true`
option:
```
$ ./bisq-cli --password=xyz --port=9998 gettrade --trade-id=<trade-id> --show-contract=true
```
The `gettrade` commands output shows the state of the trade from initial preparation through completion and closure.
Output columns include:
```
Deposit Published YES if the taker fee tx deposit has been broadcast to the network.
Deposit Confirmed YES if the taker fee tx deposit has been confirmed by the network.
Fiat Sent YES if the buyer has sent a “payment started” message to seller.
Fiat Received YES if the seller has sent a “payment received” message to buyer.
Payout Published YES if the sellers BTC payout tx has been broadcast to the network.
Withdrawn YES if the buyers BTC proceeds have been sent to an external wallet.
```
Trade status information informs both sides of a trade which steps have been completed, and which step to perform next.
It should be frequently checked by both sides before proceeding to the next step of the protocol.
_Note: There is some delay after a new trade is created due to the time it takes for a takers trade deposit fee
transaction to be published and confirmed on the bitcoin network. Both sides of the trade can check the `gettrade`
output's `Deposit Published` and `Deposit Confirmed` columns to find out when this early phase of the trade protocol is
complete._
Once the taker fee transaction has been confirmed, payment can be sent, payment receipt confirmed, and the trade
protocol completed. There are three CLI commands that must be performed in coordinated order by each side of the trade:
```
confirmpaymentstarted Buyer sends seller a message confirming payment has been sent.
confirmpaymentreceived Seller sends buyer a message confirming payment has been received.
keepfunds Keep trade proceeds in their Bisq wallets.
OR
withdrawfunds Send trade proceeds to an external wallet.
```
The last two mutually exclusive commands (`keepfunds` or `withdrawfunds`) may seem unnecessary, but they are critical
because they inform the Bisq node that a trades state can be set to `CLOSED`. Please close out your trades with one
or the other command.
Each of the CLI commands above takes one argument: `--trade-id=<trade-id>`:
```
$ ./bisq-cli --password=xyz --port=9998 confirmpaymentstarted --trade-id=<trade-id>
$ ./bisq-cli --password=xyz --port=9999 confirmpaymentreceived --trade-id=<trade-id>
$ ./bisq-cli --password=xyz --port=9998 keepfunds --trade-id=<trade-id>
$ ./bisq-cli --password=xyz --port=9999 withdrawfunds --trade-id=<trade-id> --address=<btc-address> [--memo=<"memo">]
```
## Shutting Down Test Harness
The test harness should cleanly shutdown all the background apps in proper order after entering ^C.
Once shutdown, all Bisq and bitcoin-core data files are left in the state they were in at shutdown time,
so they and logs can be examined after a test run. All datafiles will be refreshed the next time the test harness
is started, so if you want to save datafiles and logs from a test run, copy them to a safe place first.
They can be found in `apitest/build/resources/main`.

View file

@ -1,10 +1,32 @@
# Build and Run API Test Harness
# Build and Run API
## Linux & OSX
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
### 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
@ -13,7 +35,7 @@ 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
### 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
@ -29,7 +51,7 @@ Or by running a single task:
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
### 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
@ -57,12 +79,12 @@ To run test cases from Intellij, add two JVM arguments to your JUnit launchers:
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
### 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
### See also
- [test-categories.md](test-categories.md)

View file

@ -0,0 +1,28 @@
import sys, os, json
# Writes a Bisq json F2F payment account form for the given country_code to the current working directory.
if len(sys.argv) < 2:
print("usage: editf2faccountform.py country_code")
exit(1)
country_code = str(sys.argv[1]).upper()
acct_form = {
"_COMMENTS_": [
"Do not manually edit the paymentMethodId field.",
"Edit the salt field only if you are recreating a payment account on a new installation and wish to preserve the account age."
],
"paymentMethodId": "F2F",
"accountName": "Face to Face Payment Account",
"city": "Anytown",
"contact": "Me",
"country": country_code,
"extraInfo": "",
"salt": ""
}
target=os.path.dirname(os.path.realpath(__file__)) + '/' + 'f2f-acct.json'
with open (target, 'w') as outfile:
json.dump(acct_form, outfile, indent=2)
outfile.write('\n')
exit(0)

View file

@ -0,0 +1,159 @@
#! /bin/bash
# Demonstrates a way to create a limit order (offer) using the API CLI with a local regtest bitcoin node.
#
# A country code argument is used to create a country based face to face payment account for the simulated offer.
#
# Prerequisites:
#
# - Linux or OSX with bash, Java 10, or Java 11-12 (JDK language compatibility 10), and bitcoin-core (v0.19, v0.20).
#
# - 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

View file

@ -53,6 +53,8 @@
}
@test "test getversion" {
# Wait 1 second before calling getversion again.
sleep 1
load 'version-parser'
run ./bisq-cli --password=xyz getversion
[ "$status" -eq 0 ]
@ -118,6 +120,8 @@
}
@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
@ -137,7 +141,7 @@
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
[ "$output" = "wallet decrypted" ]
sleep 1
sleep 3
}
@test "test getbalance when wallet available & unlocked with 0 btc balance" {
@ -151,7 +155,7 @@
}
@test "test getunusedbsqaddress" {
run ./bisq-cli --password=xyz getfundingaddresses
run ./bisq-cli --password=xyz getunusedbsqaddress
[ "$status" -eq 0 ]
}
@ -163,6 +167,8 @@
}
@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
@ -187,16 +193,22 @@
}
@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 ]
}
@ -216,3 +228,9 @@
[ "${lines[1]}" = "Usage: bisq-cli [options] <method> [params]" ]
# TODO add asserts after help text is modified for new endpoints
}
@test "test takeoffer method --help" {
run ./bisq-cli --password=xyz takeoffer --help
[ "$status" -eq 0 ]
[ "${lines[0]}" = "takeoffer" ]
}

View file

@ -0,0 +1,119 @@
#! /bin/bash
# Demonstrates a way to always keep one offer in the market, using the API CLI with a local regtest bitcoin node.
# Alice creates an offer, waits for Bob to take it, and completes the trade protocol with him. Then Alice
# creates a new offer...
#
# Stop the script by entering ^C.
#
# A country code argument is used to create a country based face to face payment account for the simulated offer.
#
# Prerequisites:
#
# - Linux or OSX with bash, Java 10, or Java 11-12 (JDK language compatibility 10), and bitcoin-core (v0.19, v0.20).
#
# - Bisq must be fully built with apitest dao setup files installed.
# Build command: `./gradlew clean build :apitest:installDaoSetup`
#
# - All supporting nodes must be run locally, in dev/dao/regtest mode:
# bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon
#
# These should be run using the apitest harness. From the root project dir, run:
# `$ ./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false`
#
# - Only regtest btc can be bought or sold with the test payment account.
#
# Usage:
#
# This script must be run from the root of the project, e.g.:
#
# `$ apitest/scripts/rolling-offer-simulation.sh -d buy -c us -m 2.00 -a 0.125`
#
# Script options: -d <direction> -c <country-code> (-m <mkt-price-margin(%)> || -f <fixed-price>) -a <amount(btc)>
#
# Example:
#
# Create a buy/usd offer to sell 0.1 btc at 2% above market price, using a US face to face payment account:
#
# `$ apitest/scripts/rolling-offer-simulation.sh -d sell -c us -m 2.00 -a 0.1`
APP_BASE_NAME=$(basename "$0")
APP_HOME=$(pwd -P)
APITEST_SCRIPTS_HOME="$APP_HOME/apitest/scripts"
source "$APITEST_SCRIPTS_HOME/trade-simulation-env.sh"
source "$APITEST_SCRIPTS_HOME/trade-simulation-utils.sh"
checksetup
parseopts "$@"
printdate "Started $APP_BASE_NAME with parameters:"
printscriptparams
printbreak
registerdisputeagents
showcreatepaymentacctsteps "Alice" "$ALICE_PORT"
CMD="$CLI_BASE --port=$ALICE_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM"
printdate "ALICE CLI: $CMD"
CMD_OUTPUT=$(createpaymentacct "$CMD")
echo "$CMD_OUTPUT"
printbreak
export ALICE_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT")
export CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT")
printdate "Alice's F2F payment-account-id: $ALICE_ACCT_ID, currency-code: $CURRENCY_CODE"
exitoncommandalert $?
printbreak
printdate "Bob creates his F2F payment account."
CMD="$CLI_BASE --port=$BOB_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM"
printdate "BOB CLI: $CMD"
CMD_OUTPUT=$(createpaymentacct "$CMD")
echo "$CMD_OUTPUT"
printbreak
export BOB_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT")
export CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT")
printdate "Bob's F2F payment-account-id: $BOB_ACCT_ID, currency-code: $CURRENCY_CODE"
exitoncommandalert $?
printbreak
while : ; do
printdate "ALICE $ALICE_ROLE: Creating $DIRECTION $CURRENCY_CODE offer with payment acct $ALICE_ACCT_ID."
CURRENT_PRICE=$(getcurrentprice "$ALICE_PORT" "$CURRENCY_CODE")
exitoncommandalert $?
printdate "Current Market Price: $CURRENT_PRICE"
CMD=$(gencreateoffercommand "$ALICE_PORT" "$ALICE_ACCT_ID")
printdate "ALICE CLI: $CMD"
OFFER_ID=$(createoffer "$CMD")
exitoncommandalert $?
printdate "ALICE $ALICE_ROLE: Created offer with id: $OFFER_ID."
printbreak
sleeptraced 3
# Show Alice's new offer.
printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer."
CMD="$CLI_BASE --port=$ALICE_PORT getmyoffer --offer-id=$OFFER_ID"
printdate "ALICE CLI: $CMD"
OFFER=$($CMD)
exitoncommandalert $?
echo "$OFFER"
printbreak
sleeptraced 3
# Generate some btc blocks.
printdate "Generating btc blocks after publishing Alice's offer."
genbtcblocks 3 2
printbreak
RANDOM_WAIT=$(echo $[$RANDOM % 10 + 1])
printdate "Bob will take Alice's offer in $RANDOM_WAIT seconds..."
sleeptraced "$RANDOM_WAIT"
executetrade
exitoncommandalert $?
printbreak
done
exit 0

View file

@ -0,0 +1,312 @@
#! /bin/bash
# This file must be sourced by the main driver.
export CLI_BASE="./bisq-cli --password=xyz"
export ARBITRATOR_PORT=9997
export ALICE_PORT=9998
export BOB_PORT=9999
export F2F_ACCT_FORM="f2f-acct.json"
checkos() {
LINUX=FALSE
DARWIN=FALSE
UNAME=$(uname)
case "$UNAME" in
Linux* )
export LINUX=TRUE
;;
Darwin* )
export DARWIN=TRUE
;;
esac
if [[ "$LINUX" == "TRUE" ]]; then
printdate "Running on supported Linux OS."
elif [[ "$DARWIN" == "TRUE" ]]; then
printdate "Running on supported Mac OS."
else
printdate "Script cannot run on $OSTYPE OS, only Linux and OSX are supported."
exit 1
fi
}
checksetup() {
checkos
apitestusage() {
echo "The apitest harness must be running a local bitcoin regtest node, a seednode, an arbitration node,"
echo "Bob & Alice daemons, and bitcoin-core's bitcoin-cli must be in the system PATH."
echo ""
echo "From the project's root dir, start all supporting nodes from a terminal:"
echo "./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false"
exit 1;
}
printdate "Checking $APP_HOME for some expected directories and files."
if [ -d "$APP_HOME/apitest" ]; then
printdate "Subproject apitest exists.";
else
printdate "Error: Subproject apitest not found, maybe because you are not running the script from the project root dir."
exit 1
fi
if [ -f "$APP_HOME/bisq-cli" ]; then
printdate "The bisq-cli script exists.";
else
printdate "Error: The bisq-cli script not found, maybe because you are not running the script from the project root dir."
exit 1
fi
printdate "Checking to see local bitcoind is running, and bitcoin-cli is in PATH."
checkbitcoindrunning
checkbitcoincliinpath
printdate "Checking to see bisq servers are running."
checkseednoderunning
checkarbnoderunning
checkalicenoderunning
checkbobnoderunning
}
parseopts() {
usage() {
echo "Usage: $0 [-d buy|sell] [-c <country-code>] [-f <fixed-price> || -m <margin-from-price>] [-a <amount in btc>]" 1>&2
exit 1;
}
local OPTIND o d c f m a
while getopts "d:c:f:m:a:" o; do
case "${o}" in
d) d=$(echo "${OPTARG}" | tr '[:lower:]' '[:upper:]')
((d == "BUY" || d == "SELL")) || usage
export DIRECTION=${d}
;;
c) c=$(echo "${OPTARG}"| tr '[:lower:]' '[:upper:]')
export COUNTRY_CODE=${c}
;;
f) f=${OPTARG}
export FIXED_PRICE=${f}
;;
m) m=${OPTARG}
export MKT_PRICE_MARGIN=${m}
;;
a) a=${OPTARG}
export AMOUNT=${a}
;;
*) usage ;;
esac
done
shift $((OPTIND-1))
if [ -z "${d}" ] || [ -z "${c}" ] || [ -z "${a}" ]; then
usage
fi
if [ -z "${f}" ] && [ -z "${m}" ]; then
usage
fi
if [ -n "${f}" ] && [ -n "${m}" ]; then
printdate "Must use margin-from-price param (-m) or fixed-price param (-f), not both."
usage
fi
if [ "$DIRECTION" = "SELL" ]
then
export BOB_ROLE="(taker/buyer)"
export ALICE_ROLE="(maker/seller)"
else
export BOB_ROLE="(taker/seller)"
export ALICE_ROLE="(maker/buyer)"
fi
}
parselimitorderopts() {
usage() {
echo "Usage: $0 [-l limit-price] [-d buy|sell] [-c <country-code>] [-f <fixed-price> || -m <margin-from-price>] [-a <amount in btc>] [-w <price-poll-interval(s)>]" 1>&2
exit 1;
}
local OPTIND o l d c f m a w
while getopts "l:d:c:f:m:a:w:" o; do
case "${o}" in
l) l=${OPTARG}
export LIMIT_PRICE=${l}
;;
d) d=$(echo "${OPTARG}" | tr '[:lower:]' '[:upper:]')
((d == "BUY" || d == "SELL")) || usage
export DIRECTION=${d}
;;
c) c=$(echo "${OPTARG}"| tr '[:lower:]' '[:upper:]')
export COUNTRY_CODE=${c}
;;
f) f=${OPTARG}
export FIXED_PRICE=${f}
;;
m) m=${OPTARG}
export MKT_PRICE_MARGIN=${m}
;;
a) a=${OPTARG}
export AMOUNT=${a}
;;
w) w=${OPTARG}
export WAIT=${w}
;;
*) usage ;;
esac
done
shift $((OPTIND-1))
if [ -z "${l}" ]; then
usage
fi
if [ -z "${d}" ] || [ -z "${c}" ] || [ -z "${a}" ]; then
usage
fi
if [ -z "${f}" ] && [ -z "${m}" ]; then
usage
fi
if [ -n "${f}" ] && [ -n "${m}" ]; then
printdate "Must use margin-from-price param (-m) or fixed-price param (-f), not both."
usage
fi
if [ -z "${w}" ]; then
WAIT=120
elif [ "$w" -lt 20 ]; then
printdate "The -w <price-poll-interval(s)> option is too low, minimum allowed is 20s. Using default 120s."
WAIT=120
fi
}
checkbitcoindrunning() {
# There may be a '+' char in the path and we have to escape it for pgrep.
if [[ $APP_HOME == *"+"* ]]; then
ESCAPED_APP_HOME=$(escapepluschar "$APP_HOME")
else
ESCAPED_APP_HOME="$APP_HOME"
fi
if pgrep -f "bitcoind -datadir=$ESCAPED_APP_HOME/apitest/build/resources/main/Bitcoin-regtest" > /dev/null ; then
printdate "The regtest bitcoind node is running on host."
else
printdate "Error: regtest bitcoind node is not running on host, exiting."
apitestusage
fi
}
checkbitcoincliinpath() {
if which bitcoin-cli > /dev/null ; then
printdate "The bitcoin-cli binary is in the system PATH."
else
printdate "Error: bitcoin-cli binary is not in the system PATH, exiting."
apitestusage
fi
}
checkseednoderunning() {
if [[ "$LINUX" == "TRUE" ]]; then
if pgrep -f "bisq.seednode.SeedNodeMain" > /dev/null ; then
printdate "The seed node is running on host."
else
printdate "Error: seed node is not running on host, exiting."
apitestusage
fi
elif [[ "$DARWIN" == "TRUE" ]]; then
if ps -A | awk '/[S]eedNodeMain/ {print $1}' > /dev/null ; then
printdate "The seednode is running on host."
else
printdate "Error: seed node is not running on host, exiting."
apitestusage
fi
else
printdate "Error: seed node is not running on host, exiting."
apitestusage
fi
}
checkarbnoderunning() {
if [[ "$LINUX" == "TRUE" ]]; then
if pgrep -f "bisq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Arb_dao" > /dev/null ; then
printdate "The arbitration node is running on host."
else
printdate "Error: arbitration node is not running on host, exiting."
apitestusage
fi
elif [[ "$DARWIN" == "TRUE" ]]; then
if ps -A | awk '/[b]isq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Arb_dao/ {print $1}' > /dev/null ; then
printdate "The arbitration node is running on host."
else
printdate "Error: arbitration node is not running on host, exiting."
apitestusage
fi
else
printdate "Error: arbitration node is not running on host, exiting."
apitestusage
fi
}
checkalicenoderunning() {
if [[ "$LINUX" == "TRUE" ]]; then
if pgrep -f "bisq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Alice_dao" > /dev/null ; then
printdate "Alice's node is running on host."
else
printdate "Error: Alice's node is not running on host, exiting."
apitestusage
fi
elif [[ "$DARWIN" == "TRUE" ]]; then
if ps -A | awk '/[b]isq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Alice_dao/ {print $1}' > /dev/null ; then
printdate "Alice's node node is running on host."
else
printdate "Error: Alice's node is not running on host, exiting."
apitestusage
fi
else
printdate "Error: Alice's node is not running on host, exiting."
apitestusage
fi
}
checkbobnoderunning() {
if [[ "$LINUX" == "TRUE" ]]; then
if pgrep -f "bisq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Alice_dao" > /dev/null ; then
printdate "Bob's node is running on host."
else
printdate "Error: Bob's node is not running on host, exiting."
apitestusage
fi
elif [[ "$DARWIN" == "TRUE" ]]; then
if ps -A | awk '/[b]isq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Alice_dao/ {print $1}' > /dev/null ; then
printdate "Bob's node node is running on host."
else
printdate "Error: Bob's node is not running on host, exiting."
apitestusage
fi
else
printdate "Error: Bob's node is not running on host, exiting."
apitestusage
fi
}
printscriptparams() {
if [ -n "${LIMIT_PRICE+1}" ]; then
echo " LIMIT_PRICE = $LIMIT_PRICE"
fi
echo " DIRECTION = $DIRECTION"
echo " COUNTRY_CODE = $COUNTRY_CODE"
echo " FIXED_PRICE = $FIXED_PRICE"
echo " MKT_PRICE_MARGIN = $MKT_PRICE_MARGIN"
echo " AMOUNT = $AMOUNT"
if [ -n "${BOB_ROLE+1}" ]; then
echo " BOB_ROLE = $BOB_ROLE"
fi
if [ -n "${ALICE_ROLE+1}" ]; then
echo " ALICE_ROLE = $ALICE_ROLE"
fi
if [ -n "${WAIT+1}" ]; then
echo " WAIT = $WAIT"
fi
}

View file

@ -0,0 +1,603 @@
#! /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"
MAKER_OR_TAKER="$2"
if [ "$MAKER_OR_TAKER" = "MAKER" ]
then
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $9}')
else
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $10}')
fi
commandalert $? "Could not parse istradedepositpublished from trade detail."
echo "$ANSWER"
}
istradedepositconfirmed() {
TRADE_DETAIL="$1"
MAKER_OR_TAKER="$2"
if [ "$MAKER_OR_TAKER" = "MAKER" ]
then
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $10}')
else
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $11}')
fi
commandalert $? "Could not parse istradedepositconfirmed from trade detail."
echo "$ANSWER"
}
istradepaymentsent() {
TRADE_DETAIL="$1"
MAKER_OR_TAKER="$2"
if [ "$MAKER_OR_TAKER" = "MAKER" ]
then
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $12}')
else
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $13}')
fi
commandalert $? "Could not parse istradepaymentsent from trade detail."
echo "$ANSWER"
}
istradepaymentreceived() {
TRADE_DETAIL="$1"
MAKER_OR_TAKER="$2"
if [ "$MAKER_OR_TAKER" = "MAKER" ]
then
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $13}')
else
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $14}')
fi
commandalert $? "Could not parse istradepaymentreceived from trade detail."
echo "$ANSWER"
}
istradepayoutpublished() {
TRADE_DETAIL="$1"
MAKER_OR_TAKER="$2"
if [ "$MAKER_OR_TAKER" = "MAKER" ]
then
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $14}')
else
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $15}')
fi
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" "TAKER")
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" "TAKER")
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"
MAKER_OR_TAKER="$4"
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" "$MAKER_OR_TAKER")
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"
MAKER_OR_TAKER="$4"
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" "$MAKER_OR_TAKER")
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" "TAKER"
else
# Alice waits for payment, polling status in maker specific trade detail.
waitfortradepaymentsent "$PAYEE_PORT" "$PAYEE" "$OFFER_ID" "MAKER"
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" "MAKER"
else
# Bob waits for payment rcvd confirm from Alice, polling status in taker specific trade detail.
waitfortradepaymentreceived "$PAYER_PORT" "$PAYER" "$OFFER_ID" "TAKER"
fi
# Generate some btc blocks
printdate "Generating btc blocks after fiat transfer."
genbtcblocks 2 2
printbreak
# Complete the trade on the seller side.
if [ "$DIRECTION" = "BUY" ]
then
printdate "BOB $BOB_ROLE: Closing trade by keeping funds in Bisq wallet."
CMD="$CLI_BASE --port=$BOB_PORT keepfunds --trade-id=$OFFER_ID"
printdate "BOB CLI: $CMD"
else
printdate "ALICE (taker): Closing trade by keeping funds in Bisq wallet."
CMD="$CLI_BASE --port=$ALICE_PORT keepfunds --trade-id=$OFFER_ID"
printdate "ALICE CLI: $CMD"
fi
KEEP_FUNDS_MSG=$($CMD)
commandalert $? "Could close trade with keepfunds command."
# Print the keepfunds command's console output.
printdate "$KEEP_FUNDS_MSG"
sleeptraced 3
printbreak
printdate "Trade $OFFER_ID complete."
}
getcurrentprice() {
PORT="$1"
CURRENCY_CODE="$2"
CMD="$CLI_BASE --port=$PORT getbtcprice --currency-code=$CURRENCY_CODE"
CMD_OUTPUT=$($CMD)
commandalert $? "Could not get current market $CURRENCY_CODE price."
FLOOR=$(echo "$CMD_OUTPUT" | cut -d'.' -f 1)
commandalert $? "Could not get the floor of the current market $CURRENCY_CODE price."
INTEGER=$(echo "$FLOOR" | tr -cd '[[:digit:]]')
commandalert $? "Could not convert the current market $CURRENCY_CODE price string to an integer."
echo "$INTEGER"
}

View file

@ -0,0 +1,126 @@
#! /bin/bash
# Runs fiat <-> btc trading scenarios using the API CLI with a local regtest bitcoin node.
#
# A country code argument is used to create a country based face to face payment account for the simulated
# trade, and the maker's face to face payment account's currency code is used when creating the offer.
#
# Prerequisites:
#
# - Linux or OSX with bash, Java 10, or Java 11-12 (JDK language compatibility 10), and bitcoin-core (v0.19, v0.20).
#
# - Bisq must be fully built with apitest dao setup files installed.
# Build command: `./gradlew clean build :apitest:installDaoSetup`
#
# - All supporting nodes must be run locally, in dev/dao/regtest mode:
# bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon
#
# These should be run using the apitest harness. From the root project dir, run:
# `$ ./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false`
#
# - Only regtest btc can be bought or sold with the test payment account.
#
# Usage:
#
# This script must be run from the root of the project, e.g.:
#
# `$ apitest/scripts/trade-simulation.sh -d buy -c fr -m 3.00 -a 0.125`
#
# Script options: -d <direction> -c <country-code> -m <mkt-price-margin(%)> - f <fixed-price> -a <amount(btc)>
#
# Examples:
#
# Create a buy/eur offer to buy 0.125 btc at a mkt-price-margin of 0%, using an Italy face to face payment account:
#
# `$ apitest/scripts/trade-simulation.sh -d buy -c it -m 0.00 -a 0.125`
#
# Create a sell/eur offer to sell 0.125 btc at a fixed-price of 38,000 euros, using a France face to face
# payment account:
#
# `$ apitest/scripts/trade-simulation.sh -d sell -c fr -f 38000 -a 0.125`
export APP_BASE_NAME=$(basename "$0")
export APP_HOME=$(pwd -P)
export APITEST_SCRIPTS_HOME="$APP_HOME/apitest/scripts"
source "$APITEST_SCRIPTS_HOME/trade-simulation-env.sh"
source "$APITEST_SCRIPTS_HOME/trade-simulation-utils.sh"
checksetup
parseopts "$@"
printdate "Started $APP_BASE_NAME with parameters:"
printscriptparams
printbreak
registerdisputeagents
# Demonstrate how to create a country based, face to face account.
showcreatepaymentacctsteps "Alice" "$ALICE_PORT"
CMD="$CLI_BASE --port=$ALICE_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM"
printdate "ALICE CLI: $CMD"
CMD_OUTPUT=$(createpaymentacct "$CMD")
echo "$CMD_OUTPUT"
printbreak
export ALICE_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT")
export CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT")
printdate "Alice's F2F payment-account-id: $ALICE_ACCT_ID, currency-code: $CURRENCY_CODE"
exitoncommandalert $?
printbreak
printdate "Bob creates his F2F payment account."
CMD="$CLI_BASE --port=$BOB_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM"
printdate "BOB CLI: $CMD"
CMD_OUTPUT=$(createpaymentacct "$CMD")
echo "$CMD_OUTPUT"
printbreak
export BOB_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT")
export CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT")
printdate "Bob's F2F payment-account-id: $BOB_ACCT_ID, currency-code: $CURRENCY_CODE"
exitoncommandalert $?
printbreak
# Alice creates an offer.
printdate "ALICE $ALICE_ROLE: Creating $DIRECTION $CURRENCY_CODE offer with payment acct $ALICE_ACCT_ID."
CURRENT_PRICE=$(getcurrentprice "$ALICE_PORT" "$CURRENCY_CODE")
exitoncommandalert $?
printdate "Current Market Price: $CURRENT_PRICE"
CMD=$(gencreateoffercommand "$ALICE_PORT" "$ALICE_ACCT_ID")
printdate "ALICE CLI: $CMD"
OFFER_ID=$(createoffer "$CMD")
exitoncommandalert $?
printdate "ALICE $ALICE_ROLE: Created offer with id: $OFFER_ID."
printbreak
sleeptraced 3
# Show Alice's new offer.
printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer."
CMD="$CLI_BASE --port=$ALICE_PORT getmyoffer --offer-id=$OFFER_ID"
printdate "ALICE CLI: $CMD"
OFFER=$($CMD)
exitoncommandalert $?
echo "$OFFER"
printbreak
sleeptraced 3
# Generate some btc blocks.
printdate "Generating btc blocks after publishing Alice's offer."
genbtcblocks 3 1
printbreak
# Go through the trade protocol.
executetrade
exitoncommandalert $?
printbreak
# Get balances after trade completion.
printdate "Bob & Alice's balances after trade:"
printdate "ALICE CLI:"
printbalances "$ALICE_PORT"
printbreak
printdate "BOB CLI:"
printbalances "$BOB_PORT"
printbreak
exit 0

View file

@ -55,7 +55,7 @@ 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.BisqApp;
import bisq.apitest.linux.BisqProcess;
import bisq.apitest.linux.BitcoinDaemon;
import bisq.apitest.linux.LinuxProcess;
@ -367,25 +367,25 @@ public class Scaffold {
CountDownLatch countdownLatch)
throws IOException, InterruptedException {
BisqApp bisqApp = createBisqApp(bisqAppConfig);
BisqProcess bisqProcess = createBisqProcess(bisqAppConfig);
switch (bisqAppConfig) {
case seednode:
seedNodeTask = new SetupTask(bisqApp, countdownLatch);
seedNodeTask = new SetupTask(bisqProcess, countdownLatch);
seedNodeTaskFuture = executor.submit(seedNodeTask);
break;
case arbdaemon:
case arbdesktop:
arbNodeTask = new SetupTask(bisqApp, countdownLatch);
arbNodeTask = new SetupTask(bisqProcess, countdownLatch);
arbNodeTaskFuture = executor.submit(arbNodeTask);
break;
case alicedaemon:
case alicedesktop:
aliceNodeTask = new SetupTask(bisqApp, countdownLatch);
aliceNodeTask = new SetupTask(bisqProcess, countdownLatch);
aliceNodeTaskFuture = executor.submit(aliceNodeTask);
break;
case bobdaemon:
case bobdesktop:
bobNodeTask = new SetupTask(bisqApp, countdownLatch);
bobNodeTask = new SetupTask(bisqProcess, countdownLatch);
bobNodeTaskFuture = executor.submit(bobNodeTask);
break;
default:
@ -393,18 +393,18 @@ public class Scaffold {
}
log.info("Giving {} ms for {} to initialize ...", config.bisqAppInitTime, bisqAppConfig.appName);
MILLISECONDS.sleep(config.bisqAppInitTime);
if (bisqApp.hasStartupExceptions()) {
bisqApp.logExceptions(bisqApp.getStartupExceptions(), log);
throw new IllegalStateException(bisqApp.getStartupExceptions().get(0));
if (bisqProcess.hasStartupExceptions()) {
bisqProcess.logExceptions(bisqProcess.getStartupExceptions(), log);
throw new IllegalStateException(bisqProcess.getStartupExceptions().get(0));
}
}
private BisqApp createBisqApp(BisqAppConfig bisqAppConfig)
private BisqProcess createBisqProcess(BisqAppConfig bisqAppConfig)
throws IOException, InterruptedException {
BisqApp bisqNode = new BisqApp(bisqAppConfig, config);
bisqNode.verifyAppNotRunning();
bisqNode.verifyAppDataDirInstalled();
return bisqNode;
BisqProcess bisqProcess = new BisqProcess(bisqAppConfig, config);
bisqProcess.verifyAppNotRunning();
bisqProcess.verifyAppDataDirInstalled();
return bisqProcess;
}
private void verifyStartupCompleted()

View file

@ -41,7 +41,7 @@ import bisq.daemon.app.BisqDaemonMain;
* Runs a regtest/dao Bisq application instance in the background.
*/
@Slf4j
public class BisqApp extends AbstractLinuxProcess implements LinuxProcess {
public class BisqProcess extends AbstractLinuxProcess implements LinuxProcess {
private final BisqAppConfig bisqAppConfig;
private final String baseCurrencyNetwork;
@ -55,7 +55,7 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess {
private final String findBisqPidScript;
private final String debugOpts;
public BisqApp(BisqAppConfig bisqAppConfig, ApiTestConfig config) {
public BisqProcess(BisqAppConfig bisqAppConfig, ApiTestConfig config) {
super(bisqAppConfig.appName, config);
this.bisqAppConfig = bisqAppConfig;
this.baseCurrencyNetwork = "BTC_REGTEST";

View file

@ -17,26 +17,35 @@
package bisq.apitest;
import java.net.InetAddress;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
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.config.BisqAppConfig;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.cli.GrpcStubs;
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'.
@ -50,8 +59,8 @@ import bisq.cli.GrpcStubs;
* <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 default
* PerfectMoney dummy payment accounts (USD) for Bob and Alice.
* 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>
@ -63,22 +72,28 @@ import bisq.cli.GrpcStubs;
* <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;
// gRPC service stubs are used by method & scenario tests, but not e2e tests.
private static final Map<BisqAppConfig, GrpcStubs> grpcStubsCache = new HashMap<>();
@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 {
scaffold = new Scaffold(stream(supportingApps).map(Enum::name)
.collect(Collectors.joining(",")))
.setUp();
config = scaffold.config;
bitcoinCli = new BitcoinCliHelper((config));
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)
@ -90,24 +105,28 @@ public class ApiTestCase {
scaffold = new Scaffold(params).setUp();
config = scaffold.config;
bitcoinCli = new BitcoinCliHelper((config));
createGrpcClients();
}
public static void tearDownScaffold() {
scaffold.tearDown();
}
protected static String getEnumArrayAsString(Enum<?>[] supportingApps) {
return stream(supportingApps).map(Enum::name).collect(Collectors.joining(","));
}
protected static GrpcStubs grpcStubs(BisqAppConfig bisqAppConfig) {
if (grpcStubsCache.containsKey(bisqAppConfig)) {
return grpcStubsCache.get(bisqAppConfig);
} else {
GrpcStubs stubs = new GrpcStubs(InetAddress.getLoopbackAddress().getHostAddress(),
bisqAppConfig.apiPort, config.apiPassword);
grpcStubsCache.put(bisqAppConfig, stubs);
return stubs;
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);
}
}
@ -129,4 +148,37 @@ public class ApiTestCase {
? testInfo.getTestMethod().get().getName()
: "unknown test name";
}
protected static File defaultRateMeterInterceptorConfig() {
GrpcServiceRateMeteringConfig.Builder builder = new GrpcServiceRateMeteringConfig.Builder();
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
getGetVersionMethod().getFullMethodName(),
1,
SECONDS);
// Only GrpcVersionService is @VisibleForTesting, so we need to
// hardcode other grpcServiceClassName parameter values used in
// builder.addCallRateMeter(...).
builder.addCallRateMeter("GrpcDisputeAgentsService",
getRegisterDisputeAgentMethod().getFullMethodName(),
10, // Same as default.
SECONDS);
// Define rate meters for non-existent method 'disabled', to override other grpc
// services' default rate meters -- defined in their rateMeteringInterceptor()
// methods.
String[] serviceClassNames = new String[]{
"GrpcGetTradeStatisticsService",
"GrpcHelpService",
"GrpcOffersService",
"GrpcPaymentAccountsService",
"GrpcPriceService",
"GrpcTradesService",
"GrpcWalletsService"
};
for (String service : serviceClassNames) {
builder.addCallRateMeter(service, "disabled", 1, MILLISECONDS);
}
File file = builder.build();
file.deleteOnExit();
return file;
}
}

View file

@ -19,8 +19,6 @@ package bisq.apitest.method;
import io.grpc.StatusRuntimeException;
import java.io.File;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
@ -34,18 +32,9 @@ import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import bisq.daemon.grpc.GrpcVersionService;
import bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@ -55,9 +44,7 @@ public class CallRateMeteringInterceptorTest extends MethodTest {
@BeforeAll
public static void setUp() {
File callRateMeteringConfigFile = buildInterceptorConfigFile();
startSupportingApps(callRateMeteringConfigFile,
false,
startSupportingApps(false,
false,
bitcoind, alicedaemon);
}
@ -100,30 +87,4 @@ public class CallRateMeteringInterceptorTest extends MethodTest {
public static void tearDown() {
tearDownScaffold();
}
public static File buildInterceptorConfigFile() {
GrpcServiceRateMeteringConfig.Builder builder = new GrpcServiceRateMeteringConfig.Builder();
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
"getVersion",
1,
SECONDS);
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
"shouldNotBreakAnything",
1000,
DAYS);
// Only GrpcVersionService is @VisibleForTesting, so we hardcode the class names.
builder.addCallRateMeter("GrpcOffersService",
"createOffer",
5,
MINUTES);
builder.addCallRateMeter("GrpcOffersService",
"takeOffer",
10,
DAYS);
builder.addCallRateMeter("GrpcTradesService",
"withdrawFunds",
3,
HOURS);
return builder.build();
}
}

View file

@ -17,8 +17,6 @@
package bisq.apitest.method;
import bisq.proto.grpc.GetMethodHelpRequest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
@ -51,10 +49,7 @@ public class GetMethodHelpTest extends MethodTest {
@Test
@Order(1)
public void testGetCreateOfferHelp() {
var help = grpcStubs(alicedaemon).helpService
.getMethodHelp(GetMethodHelpRequest.newBuilder()
.setMethodName(createoffer.name()).build())
.getMethodHelp();
var help = aliceClient.getMethodHelp(createoffer);
assertNotNull(help);
}

View file

@ -17,8 +17,6 @@
package bisq.apitest.method;
import bisq.proto.grpc.GetVersionRequest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
@ -51,8 +49,7 @@ public class GetVersionTest extends MethodTest {
@Test
@Order(1)
public void testGetVersion() {
var version = grpcStubs(alicedaemon).versionService
.getVersion(GetVersionRequest.newBuilder().build()).getVersion();
var version = aliceClient.getVersion();
assertEquals(VERSION, version);
}

View file

@ -18,76 +18,27 @@
package bisq.apitest.method;
import bisq.core.api.model.PaymentAccountForm;
import bisq.core.api.model.TxFeeRateInfo;
import bisq.core.payment.F2FAccount;
import bisq.core.proto.CoreProtoResolver;
import bisq.common.util.Utilities;
import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.BsqBalanceInfo;
import bisq.proto.grpc.BtcBalanceInfo;
import bisq.proto.grpc.CancelOfferRequest;
import bisq.proto.grpc.ConfirmPaymentReceivedRequest;
import bisq.proto.grpc.ConfirmPaymentStartedRequest;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetMethodHelpRequest;
import bisq.proto.grpc.GetMyOfferRequest;
import bisq.proto.grpc.GetOfferRequest;
import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
import bisq.proto.grpc.KeepFundsRequest;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TradeInfo;
import bisq.proto.grpc.TxInfo;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WithdrawFundsRequest;
import protobuf.PaymentAccount;
import protobuf.PaymentMethod;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.arbdaemon;
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
import static bisq.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.stream;
import static java.util.Comparator.comparing;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import bisq.apitest.ApiTestCase;
import bisq.apitest.config.BisqAppConfig;
import bisq.cli.GrpcStubs;
import bisq.cli.GrpcClient;
public class MethodTest extends ApiTestCase {
@ -95,13 +46,7 @@ public class MethodTest extends ApiTestCase {
protected static final String MEDIATOR = "mediator";
protected static final String REFUND_AGENT = "refundagent";
protected static GrpcStubs aliceStubs;
protected static GrpcStubs bobStubs;
protected static PaymentAccount alicesDummyAcct;
protected static PaymentAccount bobsDummyAcct;
private static final CoreProtoResolver CORE_PROTO_RESOLVER = new CoreProtoResolver();
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(","));
@ -110,13 +55,25 @@ public class MethodTest extends ApiTestCase {
boolean registerDisputeAgents,
boolean generateBtcBlock,
Enum<?>... supportingApps) {
startSupportingApps(callRateMeteringConfigFile,
registerDisputeAgents,
generateBtcBlock,
false,
supportingApps);
}
public static void startSupportingApps(File callRateMeteringConfigFile,
boolean registerDisputeAgents,
boolean generateBtcBlock,
boolean startSupportingAppsInDebugMode,
Enum<?>... supportingApps) {
try {
setUpScaffold(new String[]{
"--supportingApps", toNameList.apply(supportingApps),
"--callRateMeteringConfigPath", callRateMeteringConfigFile.getAbsolutePath(),
"--enableBisqDebugging", "false"
"--enableBisqDebugging", startSupportingAppsInDebugMode ? "true" : "false"
});
doPostStartup(registerDisputeAgents, generateBtcBlock, supportingApps);
doPostStartup(registerDisputeAgents, generateBtcBlock);
} catch (Exception ex) {
fail(ex);
}
@ -125,32 +82,34 @@ public class MethodTest extends ApiTestCase {
public static void startSupportingApps(boolean registerDisputeAgents,
boolean generateBtcBlock,
Enum<?>... supportingApps) {
startSupportingApps(registerDisputeAgents,
generateBtcBlock,
false,
supportingApps);
}
public static void startSupportingApps(boolean registerDisputeAgents,
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),
"--enableBisqDebugging", "false"
"--callRateMeteringConfigPath", callRateMeteringConfigFile.getAbsolutePath(),
"--enableBisqDebugging", startSupportingAppsInDebugMode ? "true" : "false"
});
doPostStartup(registerDisputeAgents, generateBtcBlock, supportingApps);
doPostStartup(registerDisputeAgents, generateBtcBlock);
} catch (Exception ex) {
fail(ex);
}
}
protected static void doPostStartup(boolean registerDisputeAgents,
boolean generateBtcBlock,
Enum<?>... supportingApps) {
boolean generateBtcBlock) {
if (registerDisputeAgents) {
registerDisputeAgents(arbdaemon);
}
if (stream(supportingApps).map(Enum::name).anyMatch(name -> name.equals(alicedaemon.name()))) {
aliceStubs = grpcStubs(alicedaemon);
alicesDummyAcct = getDefaultPerfectDummyPaymentAccount(alicedaemon);
}
if (stream(supportingApps).map(Enum::name).anyMatch(name -> name.equals(bobdaemon.name()))) {
bobStubs = grpcStubs(bobdaemon);
bobsDummyAcct = getDefaultPerfectDummyPaymentAccount(bobdaemon);
registerDisputeAgents();
}
// Generate 1 regtest block for alice's and/or bob's wallet to
@ -159,212 +118,11 @@ public class MethodTest extends ApiTestCase {
genBtcBlocksThenWait(1, 1500);
}
// Convenience methods for building gRPC request objects
protected final GetBalancesRequest createGetBalancesRequest(String currencyCode) {
return GetBalancesRequest.newBuilder().setCurrencyCode(currencyCode).build();
}
protected final GetAddressBalanceRequest createGetAddressBalanceRequest(String address) {
return GetAddressBalanceRequest.newBuilder().setAddress(address).build();
}
protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) {
return SetWalletPasswordRequest.newBuilder().setPassword(password).build();
}
protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String oldPassword, String newPassword) {
return SetWalletPasswordRequest.newBuilder().setPassword(oldPassword).setNewPassword(newPassword).build();
}
protected final RemoveWalletPasswordRequest createRemoveWalletPasswordRequest(String password) {
return RemoveWalletPasswordRequest.newBuilder().setPassword(password).build();
}
protected final UnlockWalletRequest createUnlockWalletRequest(String password, long timeout) {
return UnlockWalletRequest.newBuilder().setPassword(password).setTimeout(timeout).build();
}
protected final LockWalletRequest createLockWalletRequest() {
return LockWalletRequest.newBuilder().build();
}
protected final GetUnusedBsqAddressRequest createGetUnusedBsqAddressRequest() {
return GetUnusedBsqAddressRequest.newBuilder().build();
}
protected final SendBsqRequest createSendBsqRequest(String address,
String amount,
String txFeeRate) {
return SendBsqRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.build();
}
protected final SendBtcRequest createSendBtcRequest(String address,
String amount,
String txFeeRate,
String memo) {
return SendBtcRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.setMemo(memo)
.build();
}
protected final GetFundingAddressesRequest createGetFundingAddressesRequest() {
return GetFundingAddressesRequest.newBuilder().build();
}
protected final MarketPriceRequest createMarketPriceRequest(String currencyCode) {
return MarketPriceRequest.newBuilder().setCurrencyCode(currencyCode).build();
}
protected final GetOfferRequest createGetOfferRequest(String offerId) {
return GetOfferRequest.newBuilder().setId(offerId).build();
}
protected final GetMyOfferRequest createGetMyOfferRequest(String offerId) {
return GetMyOfferRequest.newBuilder().setId(offerId).build();
}
protected final CancelOfferRequest createCancelOfferRequest(String offerId) {
return CancelOfferRequest.newBuilder().setId(offerId).build();
}
protected final TakeOfferRequest createTakeOfferRequest(String offerId,
String paymentAccountId,
String takerFeeCurrencyCode) {
return TakeOfferRequest.newBuilder()
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId)
.setTakerFeeCurrencyCode(takerFeeCurrencyCode)
.build();
}
protected final GetTradeRequest createGetTradeRequest(String tradeId) {
return GetTradeRequest.newBuilder().setTradeId(tradeId).build();
}
protected final ConfirmPaymentStartedRequest createConfirmPaymentStartedRequest(String tradeId) {
return ConfirmPaymentStartedRequest.newBuilder().setTradeId(tradeId).build();
}
protected final ConfirmPaymentReceivedRequest createConfirmPaymentReceivedRequest(String tradeId) {
return ConfirmPaymentReceivedRequest.newBuilder().setTradeId(tradeId).build();
}
protected final KeepFundsRequest createKeepFundsRequest(String tradeId) {
return KeepFundsRequest.newBuilder()
.setTradeId(tradeId)
.build();
}
protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId,
String address,
String memo) {
return WithdrawFundsRequest.newBuilder()
.setTradeId(tradeId)
.setAddress(address)
.setMemo(memo)
.build();
}
protected final GetMethodHelpRequest createGetMethodHelpRequest(String methodName) {
return GetMethodHelpRequest.newBuilder().setMethodName(methodName).build();
}
// Convenience methods for calling frequently used & thoroughly tested gRPC services.
protected final BalancesInfo getBalances(BisqAppConfig bisqAppConfig, String currencyCode) {
return grpcStubs(bisqAppConfig).walletsService.getBalances(
createGetBalancesRequest(currencyCode)).getBalances();
}
protected final BsqBalanceInfo getBsqBalances(BisqAppConfig bisqAppConfig) {
return getBalances(bisqAppConfig, "bsq").getBsq();
}
protected final BtcBalanceInfo getBtcBalances(BisqAppConfig bisqAppConfig) {
return getBalances(bisqAppConfig, "btc").getBtc();
}
protected final AddressBalanceInfo getAddressBalance(BisqAppConfig bisqAppConfig, String address) {
return grpcStubs(bisqAppConfig).walletsService.getAddressBalance(createGetAddressBalanceRequest(address)).getAddressBalanceInfo();
}
protected final void unlockWallet(BisqAppConfig bisqAppConfig, String password, long timeout) {
//noinspection ResultOfMethodCallIgnored
grpcStubs(bisqAppConfig).walletsService.unlockWallet(createUnlockWalletRequest(password, timeout));
}
protected final void lockWallet(BisqAppConfig bisqAppConfig) {
//noinspection ResultOfMethodCallIgnored
grpcStubs(bisqAppConfig).walletsService.lockWallet(createLockWalletRequest());
}
protected final String getUnusedBsqAddress(BisqAppConfig bisqAppConfig) {
return grpcStubs(bisqAppConfig).walletsService.getUnusedBsqAddress(createGetUnusedBsqAddressRequest()).getAddress();
}
protected final TxInfo sendBsq(BisqAppConfig bisqAppConfig,
String address,
String amount) {
return sendBsq(bisqAppConfig, address, amount, "");
}
protected final TxInfo sendBsq(BisqAppConfig bisqAppConfig,
String address,
String amount,
String txFeeRate) {
//noinspection ResultOfMethodCallIgnored
return grpcStubs(bisqAppConfig).walletsService.sendBsq(createSendBsqRequest(address,
amount,
txFeeRate))
.getTxInfo();
}
protected final TxInfo sendBtc(BisqAppConfig bisqAppConfig, String address, String amount) {
return sendBtc(bisqAppConfig, address, amount, "", "");
}
protected final TxInfo sendBtc(BisqAppConfig bisqAppConfig,
String address,
String amount,
String txFeeRate,
String memo) {
//noinspection ResultOfMethodCallIgnored
return grpcStubs(bisqAppConfig).walletsService.sendBtc(
createSendBtcRequest(address, amount, txFeeRate, memo))
.getTxInfo();
}
protected final String getUnusedBtcAddress(BisqAppConfig bisqAppConfig) {
//noinspection OptionalGetWithoutIsPresent
return grpcStubs(bisqAppConfig).walletsService.getFundingAddresses(createGetFundingAddressesRequest())
.getAddressBalanceInfoList()
.stream()
.filter(a -> a.getBalance() == 0 && a.getNumConfirmations() == 0)
.findFirst()
.get()
.getAddress();
}
protected final List<PaymentMethod> getPaymentMethods(BisqAppConfig bisqAppConfig) {
var req = GetPaymentMethodsRequest.newBuilder().build();
return grpcStubs(bisqAppConfig).paymentAccountsService.getPaymentMethods(req).getPaymentMethodsList();
}
protected final File getPaymentAccountForm(BisqAppConfig bisqAppConfig, String paymentMethodId) {
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).
var req = GetPaymentAccountFormRequest.newBuilder()
.setPaymentMethodId(paymentMethodId)
.build();
String jsonString = grpcStubs(bisqAppConfig).paymentAccountsService.getPaymentAccountForm(req)
.getPaymentAccountFormJson();
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)) {
@ -375,113 +133,9 @@ public class MethodTest extends ApiTestCase {
return jsonFile;
}
protected final bisq.core.payment.PaymentAccount createPaymentAccount(BisqAppConfig bisqAppConfig,
String jsonString) {
var req = CreatePaymentAccountRequest.newBuilder()
.setPaymentAccountForm(jsonString)
.build();
var paymentAccountsService = grpcStubs(bisqAppConfig).paymentAccountsService;
// Normally, we can do asserts on the protos from the gRPC service, but in this
// case we need to return a bisq.core.payment.PaymentAccount so it can be cast
// to its sub type.
return fromProto(paymentAccountsService.createPaymentAccount(req).getPaymentAccount());
}
protected static List<PaymentAccount> getPaymentAccounts(BisqAppConfig bisqAppConfig) {
var req = GetPaymentAccountsRequest.newBuilder().build();
return grpcStubs(bisqAppConfig).paymentAccountsService.getPaymentAccounts(req)
.getPaymentAccountsList()
.stream()
.sorted(comparing(PaymentAccount::getCreationDate))
.collect(Collectors.toList());
}
protected static PaymentAccount getDefaultPerfectDummyPaymentAccount(BisqAppConfig bisqAppConfig) {
PaymentAccount paymentAccount = getPaymentAccounts(bisqAppConfig).get(0);
assertEquals("PerfectMoney dummy", paymentAccount.getAccountName());
return paymentAccount;
}
protected final double getMarketPrice(BisqAppConfig bisqAppConfig, String currencyCode) {
var req = createMarketPriceRequest(currencyCode);
return grpcStubs(bisqAppConfig).priceService.getMarketPrice(req).getPrice();
}
protected final OfferInfo getOffer(BisqAppConfig bisqAppConfig, String offerId) {
var req = createGetOfferRequest(offerId);
return grpcStubs(bisqAppConfig).offersService.getOffer(req).getOffer();
}
protected final OfferInfo getMyOffer(BisqAppConfig bisqAppConfig, String offerId) {
var req = createGetMyOfferRequest(offerId);
return grpcStubs(bisqAppConfig).offersService.getMyOffer(req).getOffer();
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void cancelOffer(BisqAppConfig bisqAppConfig, String offerId) {
var req = createCancelOfferRequest(offerId);
grpcStubs(bisqAppConfig).offersService.cancelOffer(req);
}
protected final TradeInfo getTrade(BisqAppConfig bisqAppConfig, String tradeId) {
var req = createGetTradeRequest(tradeId);
return grpcStubs(bisqAppConfig).tradesService.getTrade(req).getTrade();
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void confirmPaymentStarted(BisqAppConfig bisqAppConfig, String tradeId) {
var req = createConfirmPaymentStartedRequest(tradeId);
grpcStubs(bisqAppConfig).tradesService.confirmPaymentStarted(req);
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void confirmPaymentReceived(BisqAppConfig bisqAppConfig, String tradeId) {
var req = createConfirmPaymentReceivedRequest(tradeId);
grpcStubs(bisqAppConfig).tradesService.confirmPaymentReceived(req);
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void keepFunds(BisqAppConfig bisqAppConfig, String tradeId) {
var req = createKeepFundsRequest(tradeId);
grpcStubs(bisqAppConfig).tradesService.keepFunds(req);
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void withdrawFunds(BisqAppConfig bisqAppConfig,
String tradeId,
String address,
String memo) {
var req = createWithdrawFundsRequest(tradeId, address, memo);
grpcStubs(bisqAppConfig).tradesService.withdrawFunds(req);
}
protected final TxFeeRateInfo getTxFeeRate(BisqAppConfig bisqAppConfig) {
var req = GetTxFeeRateRequest.newBuilder().build();
return TxFeeRateInfo.fromProto(
grpcStubs(bisqAppConfig).walletsService.getTxFeeRate(req).getTxFeeRateInfo());
}
protected final TxFeeRateInfo setTxFeeRate(BisqAppConfig bisqAppConfig, long feeRate) {
var req = SetTxFeeRatePreferenceRequest.newBuilder()
.setTxFeeRatePreference(feeRate)
.build();
return TxFeeRateInfo.fromProto(
grpcStubs(bisqAppConfig).walletsService.setTxFeeRatePreference(req).getTxFeeRateInfo());
}
protected final TxFeeRateInfo unsetTxFeeRate(BisqAppConfig bisqAppConfig) {
var req = UnsetTxFeeRatePreferenceRequest.newBuilder().build();
return TxFeeRateInfo.fromProto(
grpcStubs(bisqAppConfig).walletsService.unsetTxFeeRatePreference(req).getTxFeeRateInfo());
}
protected final TxInfo getTransaction(BisqAppConfig bisqAppConfig, String txId) {
var req = GetTransactionRequest.newBuilder().setTxId(txId).build();
return grpcStubs(bisqAppConfig).walletsService.getTransaction(req).getTxInfo();
}
public bisq.core.payment.PaymentAccount createDummyF2FAccount(BisqAppConfig bisqAppConfig,
String countryCode) {
protected bisq.core.payment.PaymentAccount createDummyF2FAccount(GrpcClient grpcClient,
String countryCode) {
String f2fAccountJsonString = "{\n" +
" \"_COMMENTS_\": \"This is a dummy account.\",\n" +
" \"paymentMethodId\": \"F2F\",\n" +
@ -491,35 +145,26 @@ public class MethodTest extends ApiTestCase {
" \"country\": \"" + countryCode.toUpperCase() + "\",\n" +
" \"extraInfo\": \"Salt Lick #213\"\n" +
"}\n";
F2FAccount f2FAccount = (F2FAccount) createPaymentAccount(bisqAppConfig, f2fAccountJsonString);
F2FAccount f2FAccount = (F2FAccount) createPaymentAccount(grpcClient, f2fAccountJsonString);
return f2FAccount;
}
protected final String getMethodHelp(BisqAppConfig bisqAppConfig, String methodName) {
var req = createGetMethodHelpRequest(methodName);
return grpcStubs(bisqAppConfig).helpService.getMethodHelp(req).getMethodHelp();
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 RegisterDisputeAgentRequest createRegisterDisputeAgentRequest(String disputeAgentType) {
return RegisterDisputeAgentRequest.newBuilder()
.setDisputeAgentType(disputeAgentType.toLowerCase())
.setRegistrationKey(DEV_PRIVILEGE_PRIV_KEY).build();
}
@SuppressWarnings({"ResultOfMethodCallIgnored", "SameParameterValue"})
protected static void registerDisputeAgents(BisqAppConfig bisqAppConfig) {
var disputeAgentsService = grpcStubs(bisqAppConfig).disputeAgentsService;
disputeAgentsService.registerDisputeAgent(createRegisterDisputeAgentRequest(MEDIATOR));
disputeAgentsService.registerDisputeAgent(createRegisterDisputeAgentRequest(REFUND_AGENT));
protected static void registerDisputeAgents() {
arbClient.registerDisputeAgent(MEDIATOR, DEV_PRIVILEGE_PRIV_KEY);
arbClient.registerDisputeAgent(REFUND_AGENT, DEV_PRIVILEGE_PRIV_KEY);
}
protected static String encodeToHex(String s) {
return Utilities.bytesAsHexString(s.getBytes(UTF_8));
}
private bisq.core.payment.PaymentAccount fromProto(PaymentAccount proto) {
return bisq.core.payment.PaymentAccount.fromProto(proto, CORE_PROTO_RESOLVER);
}
}

View file

@ -17,8 +17,6 @@
package bisq.apitest.method;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
@ -58,9 +56,8 @@ public class RegisterDisputeAgentsTest extends MethodTest {
@Test
@Order(1)
public void testRegisterArbitratorShouldThrowException() {
var req = createRegisterDisputeAgentRequest(ARBITRATOR);
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req));
arbClient.registerDisputeAgent(ARBITRATOR, DEV_PRIVILEGE_PRIV_KEY));
assertEquals("INVALID_ARGUMENT: arbitrators must be registered in a Bisq UI",
exception.getMessage());
}
@ -68,9 +65,8 @@ public class RegisterDisputeAgentsTest extends MethodTest {
@Test
@Order(2)
public void testInvalidDisputeAgentTypeArgShouldThrowException() {
var req = createRegisterDisputeAgentRequest("badagent");
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req));
arbClient.registerDisputeAgent("badagent", DEV_PRIVILEGE_PRIV_KEY));
assertEquals("INVALID_ARGUMENT: unknown dispute agent type 'badagent'",
exception.getMessage());
}
@ -78,11 +74,8 @@ public class RegisterDisputeAgentsTest extends MethodTest {
@Test
@Order(3)
public void testInvalidRegistrationKeyArgShouldThrowException() {
var req = RegisterDisputeAgentRequest.newBuilder()
.setDisputeAgentType(REFUND_AGENT)
.setRegistrationKey("invalid" + DEV_PRIVILEGE_PRIV_KEY).build();
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req));
arbClient.registerDisputeAgent(REFUND_AGENT, "invalid" + DEV_PRIVILEGE_PRIV_KEY));
assertEquals("INVALID_ARGUMENT: invalid registration key",
exception.getMessage());
}
@ -90,15 +83,13 @@ public class RegisterDisputeAgentsTest extends MethodTest {
@Test
@Order(4)
public void testRegisterMediator() {
var req = createRegisterDisputeAgentRequest(MEDIATOR);
grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req);
arbClient.registerDisputeAgent(MEDIATOR, DEV_PRIVILEGE_PRIV_KEY);
}
@Test
@Order(5)
public void testRegisterRefundAgent() {
var req = createRegisterDisputeAgentRequest(REFUND_AGENT);
grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req);
arbClient.registerDisputeAgent(REFUND_AGENT, DEV_PRIVILEGE_PRIV_KEY);
}
@AfterAll

View file

@ -18,20 +18,12 @@
package bisq.apitest.method.offer;
import bisq.core.monetary.Altcoin;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.GetMyOffersRequest;
import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.OfferInfo;
import org.bitcoinj.utils.Fiat;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
@ -44,21 +36,19 @@ 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.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static bisq.core.locale.CurrencyUtil.isCryptoCurrency;
import static java.lang.String.format;
import static java.math.RoundingMode.HALF_UP;
import static java.util.Comparator.comparing;
import static org.junit.jupiter.api.Assertions.fail;
import bisq.apitest.method.MethodTest;
import bisq.cli.GrpcStubs;
@Slf4j
public abstract class AbstractOfferTest extends MethodTest {
@Setter
protected static boolean isLongRunningTest;
@BeforeAll
public static void setUp() {
startSupportingApps(true,
@ -70,109 +60,11 @@ public abstract class AbstractOfferTest extends MethodTest {
bobdaemon);
}
protected final OfferInfo createAliceOffer(PaymentAccount paymentAccount,
String direction,
String currencyCode,
long amount,
String makerFeeCurrencyCode) {
return createMarketBasedPricedOffer(aliceStubs,
paymentAccount,
direction,
currencyCode,
amount,
makerFeeCurrencyCode);
}
protected final OfferInfo createBobOffer(PaymentAccount paymentAccount,
String direction,
String currencyCode,
long amount,
String makerFeeCurrencyCode) {
return createMarketBasedPricedOffer(bobStubs,
paymentAccount,
direction,
currencyCode,
amount,
makerFeeCurrencyCode);
}
protected final OfferInfo createMarketBasedPricedOffer(GrpcStubs grpcStubs,
PaymentAccount paymentAccount,
String direction,
String currencyCode,
long amount,
String makerFeeCurrencyCode) {
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(paymentAccount.getId())
.setDirection(direction)
.setCurrencyCode(currencyCode)
.setAmount(amount)
.setMinAmount(amount)
.setUseMarketBasedPrice(true)
.setMarketPriceMargin(0.00)
.setPrice("0")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(makerFeeCurrencyCode)
.build();
return grpcStubs.offersService.createOffer(req).getOffer();
}
protected final OfferInfo getOffer(String offerId) {
return aliceStubs.offersService.getOffer(createGetOfferRequest(offerId)).getOffer();
}
protected final OfferInfo getMyOffer(String offerId) {
return aliceStubs.offersService.getMyOffer(createGetMyOfferRequest(offerId)).getOffer();
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void cancelOffer(GrpcStubs grpcStubs, String offerId) {
grpcStubs.offersService.cancelOffer(createCancelOfferRequest(offerId));
}
protected final OfferInfo getMostRecentOffer(GrpcStubs grpcStubs, String direction, String currencyCode) {
List<OfferInfo> offerInfoList = getOffersSortedByDate(grpcStubs, direction, currencyCode);
if (offerInfoList.isEmpty())
fail(format("No %s offers found for currency %s", direction, currencyCode));
return offerInfoList.get(offerInfoList.size() - 1);
}
protected final List<OfferInfo> getOffersSortedByDate(GrpcStubs grpcStubs,
String direction,
String currencyCode) {
var req = GetOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode).build();
var reply = grpcStubs.offersService.getOffers(req);
return sortOffersByDate(reply.getOffersList());
}
protected final List<OfferInfo> getMyOffersSortedByDate(GrpcStubs grpcStubs,
String direction,
String currencyCode) {
var req = GetMyOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode).build();
var reply = grpcStubs.offersService.getMyOffers(req);
return sortOffersByDate(reply.getOffersList());
}
protected final List<OfferInfo> sortOffersByDate(List<OfferInfo> offerInfoList) {
return offerInfoList.stream()
.sorted(comparing(OfferInfo::getDate))
.collect(Collectors.toList());
}
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 getMarketPrice(String currencyCode) {
return getMarketPrice(alicedaemon, currencyCode);
}
protected final double getPercentageDifference(double price1, double price2) {
return BigDecimal.valueOf(roundDouble((1 - (price1 / price2)), 5))
.setScale(4, HALF_UP)

View file

@ -17,13 +17,12 @@
package bisq.apitest.method.offer;
import bisq.core.btc.wallet.Restrictions;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.OfferInfo;
import java.util.List;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
@ -33,7 +32,7 @@ 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.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Disabled
@ -45,45 +44,43 @@ public class CancelOfferTest extends AbstractOfferTest {
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(alicedaemon, "CA");
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(cadAccount.getId())
.setDirection(DIRECTION)
.setCurrencyCode(CURRENCY_CODE)
.setAmount(10000000)
.setMinAmount(10000000)
.setUseMarketBasedPrice(true)
.setMarketPriceMargin(0.00)
.setPrice("0")
.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode("bsq")
.build();
PaymentAccount cadAccount = createDummyF2FAccount(aliceClient, "CA");
// Create some offers.
for (int i = 1; i <= MAX_OFFERS; i++) {
//noinspection ResultOfMethodCallIgnored
aliceStubs.offersService.createOffer(req);
createOfferToCancel.accept(cadAccount.getId());
// Wait for Alice's AddToOfferBook task.
// Wait times vary; my logs show >= 2 second delay.
sleep(2500);
}
List<OfferInfo> offers = getMyOffersSortedByDate(aliceStubs, DIRECTION, CURRENCY_CODE);
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++) {
cancelOffer(aliceStubs, offers.remove(0).getId());
offers = getMyOffersSortedByDate(aliceStubs, DIRECTION, CURRENCY_CODE);
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 = getMyOffersSortedByDate(aliceStubs, DIRECTION, CURRENCY_CODE);
offers = aliceClient.getMyOffersSortedByDate(DIRECTION, CURRENCY_CODE);
assertEquals(0, offers.size());
}
}

View file

@ -19,8 +19,6 @@ package bisq.apitest.method.offer;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.CreateOfferRequest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
@ -29,7 +27,6 @@ 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.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -45,20 +42,15 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
@Test
@Order(1)
public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() {
PaymentAccount audAccount = createDummyF2FAccount(alicedaemon, "AU");
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(audAccount.getId())
.setDirection("buy")
.setCurrencyCode("aud")
.setAmount(10000000)
.setMinAmount(10000000)
.setUseMarketBasedPrice(false)
.setMarketPriceMargin(0.00)
.setPrice("36000")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
PaymentAccount audAccount = createDummyF2FAccount(aliceClient, "AU");
var newOffer = aliceClient.createFixedPricedOffer("buy",
"aud",
10000000L,
10000000L,
"36000",
getDefaultBuyerSecurityDepositAsPercent(),
audAccount.getId(),
MAKER_FEE_CURRENCY_CODE);
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals("BUY", newOffer.getDirection());
@ -72,7 +64,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertEquals("AUD", newOffer.getCounterCurrencyCode());
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getMyOffer(newOfferId);
newOffer = aliceClient.getMyOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
assertEquals("BUY", newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
@ -89,20 +81,15 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
@Test
@Order(2)
public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() {
PaymentAccount usdAccount = createDummyF2FAccount(alicedaemon, "US");
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(usdAccount.getId())
.setDirection("buy")
.setCurrencyCode("usd")
.setAmount(10000000)
.setMinAmount(10000000)
.setUseMarketBasedPrice(false)
.setMarketPriceMargin(0.00)
.setPrice("30000.1234")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
var newOffer = aliceClient.createFixedPricedOffer("buy",
"usd",
10000000L,
10000000L,
"30000.1234",
getDefaultBuyerSecurityDepositAsPercent(),
usdAccount.getId(),
MAKER_FEE_CURRENCY_CODE);
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals("BUY", newOffer.getDirection());
@ -116,7 +103,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertEquals("USD", newOffer.getCounterCurrencyCode());
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getMyOffer(newOfferId);
newOffer = aliceClient.getMyOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
assertEquals("BUY", newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
@ -133,20 +120,15 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
@Test
@Order(3)
public void testCreateEURBTCSellOfferUsingFixedPrice95001234() {
PaymentAccount eurAccount = createDummyF2FAccount(alicedaemon, "FR");
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(eurAccount.getId())
.setDirection("sell")
.setCurrencyCode("eur")
.setAmount(10000000)
.setMinAmount(10000000)
.setUseMarketBasedPrice(false)
.setMarketPriceMargin(0.00)
.setPrice("29500.1234")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
PaymentAccount eurAccount = createDummyF2FAccount(aliceClient, "FR");
var newOffer = aliceClient.createFixedPricedOffer("sell",
"eur",
10000000L,
10000000L,
"29500.1234",
getDefaultBuyerSecurityDepositAsPercent(),
eurAccount.getId(),
MAKER_FEE_CURRENCY_CODE);
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals("SELL", newOffer.getDirection());
@ -160,7 +142,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertEquals("EUR", newOffer.getCounterCurrencyCode());
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getMyOffer(newOfferId);
newOffer = aliceClient.getMyOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
assertEquals("SELL", newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());

View file

@ -19,7 +19,6 @@ package bisq.apitest.method.offer;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.OfferInfo;
import java.text.DecimalFormat;
@ -32,7 +31,6 @@ 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.util.MathUtils.scaleDownByPowerOf10;
import static bisq.common.util.MathUtils.scaleUpByPowerOf10;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
@ -57,21 +55,16 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
@Test
@Order(1)
public void testCreateUSDBTCBuyOffer5PctPriceMargin() {
PaymentAccount usdAccount = createDummyF2FAccount(alicedaemon, "US");
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
double priceMarginPctInput = 5.00;
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(usdAccount.getId())
.setDirection("buy")
.setCurrencyCode("usd")
.setAmount(10000000)
.setMinAmount(10000000)
.setUseMarketBasedPrice(true)
.setMarketPriceMargin(priceMarginPctInput)
.setPrice("0")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
var newOffer = aliceClient.createMarketBasedPricedOffer("buy",
"usd",
10000000L,
10000000L,
priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(),
usdAccount.getId(),
MAKER_FEE_CURRENCY_CODE);
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals("BUY", newOffer.getDirection());
@ -84,7 +77,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals("USD", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getMyOffer(newOfferId);
newOffer = aliceClient.getMyOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
assertEquals("BUY", newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
@ -102,8 +95,9 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
@Test
@Order(2)
public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() {
PaymentAccount nzdAccount = createDummyF2FAccount(alicedaemon, "NZ");
PaymentAccount nzdAccount = createDummyF2FAccount(aliceClient, "NZ");
double priceMarginPctInput = -2.00;
/*
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(nzdAccount.getId())
.setDirection("buy")
@ -117,6 +111,16 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
*/
var newOffer = aliceClient.createMarketBasedPricedOffer("buy",
"nzd",
10000000L,
10000000L,
priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(),
nzdAccount.getId(),
MAKER_FEE_CURRENCY_CODE);
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals("BUY", newOffer.getDirection());
@ -129,7 +133,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals("NZD", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getMyOffer(newOfferId);
newOffer = aliceClient.getMyOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
assertEquals("BUY", newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
@ -147,22 +151,16 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
@Test
@Order(3)
public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() {
PaymentAccount gbpAccount = createDummyF2FAccount(alicedaemon, "GB");
PaymentAccount gbpAccount = createDummyF2FAccount(aliceClient, "GB");
double priceMarginPctInput = -1.5;
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(gbpAccount.getId())
.setDirection("sell")
.setCurrencyCode("gbp")
.setAmount(10000000)
.setMinAmount(10000000)
.setUseMarketBasedPrice(true)
.setMarketPriceMargin(priceMarginPctInput)
.setPrice("0")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
var newOffer = aliceClient.createMarketBasedPricedOffer("sell",
"gbp",
10000000L,
10000000L,
priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(),
gbpAccount.getId(),
MAKER_FEE_CURRENCY_CODE);
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals("SELL", newOffer.getDirection());
@ -175,7 +173,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals("GBP", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getMyOffer(newOfferId);
newOffer = aliceClient.getMyOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
assertEquals("SELL", newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
@ -193,22 +191,16 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
@Test
@Order(4)
public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() {
PaymentAccount brlAccount = createDummyF2FAccount(alicedaemon, "BR");
PaymentAccount brlAccount = createDummyF2FAccount(aliceClient, "BR");
double priceMarginPctInput = 6.55;
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(brlAccount.getId())
.setDirection("sell")
.setCurrencyCode("brl")
.setAmount(10000000)
.setMinAmount(10000000)
.setUseMarketBasedPrice(true)
.setMarketPriceMargin(priceMarginPctInput)
.setPrice("0")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
var newOffer = aliceClient.createMarketBasedPricedOffer("sell",
"brl",
10000000L,
10000000L,
priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(),
brlAccount.getId(),
MAKER_FEE_CURRENCY_CODE);
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals("SELL", newOffer.getDirection());
@ -221,7 +213,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals("BRL", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getMyOffer(newOfferId);
newOffer = aliceClient.getMyOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
assertEquals("SELL", newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
@ -239,7 +231,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
private void assertCalculatedPriceIsCorrect(OfferInfo offer, double priceMarginPctInput) {
assertTrue(() -> {
String counterCurrencyCode = offer.getCounterCurrencyCode();
double mktPrice = getMarketPrice(counterCurrencyCode);
double mktPrice = aliceClient.getBtcPrice(counterCurrencyCode);
double scaledOfferPrice = getScaledOfferPrice(offer.getPrice(), counterCurrencyCode);
double expectedDiffPct = scaleDownByPowerOf10(priceMarginPctInput, 2);
double actualDiffPct = offer.getDirection().equals(BUY.name())

View file

@ -19,8 +19,6 @@ package bisq.apitest.method.offer;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.CreateOfferRequest;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
@ -31,8 +29,8 @@ 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.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;
@ -44,23 +42,54 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
@Test
@Order(1)
public void testAmtTooLargeShouldThrowException() {
PaymentAccount usdAccount = createDummyF2FAccount(alicedaemon, "US");
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(usdAccount.getId())
.setDirection("buy")
.setCurrencyCode("usd")
.setAmount(100000000000L)
.setMinAmount(100000000000L)
.setUseMarketBasedPrice(false)
.setMarketPriceMargin(0.00)
.setPrice("10000.0000")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode("bsq")
.build();
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
@SuppressWarnings("ResultOfMethodCallIgnored")
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceStubs.offersService.createOffer(req).getOffer());
aliceClient.createFixedPricedOffer("buy",
"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",
"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",
"cad",
10000000L,
10000000L,
"63000.0000",
getDefaultBuyerSecurityDepositAsPercent(),
audAccount.getId(),
"btc"));
String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId());
assertEquals(expectedError, exception.getMessage());
}
}

View file

@ -5,8 +5,6 @@ import bisq.core.locale.Res;
import bisq.core.locale.TradeCurrency;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonWriter;
@ -29,7 +27,6 @@ import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.nio.charset.StandardCharsets.UTF_8;
@ -38,6 +35,7 @@ import static org.junit.jupiter.api.Assertions.*;
import bisq.apitest.method.MethodTest;
import bisq.cli.GrpcClient;
@Slf4j
public class AbstractPaymentAccountTest extends MethodTest {
@ -87,6 +85,7 @@ public class AbstractPaymentAccountTest extends MethodTest {
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()
@ -110,7 +109,7 @@ public class AbstractPaymentAccountTest extends MethodTest {
// would be skipped.
COMPLETED_FORM_MAP.clear();
File emptyForm = getPaymentAccountForm(alicedaemon, paymentMethodId);
File emptyForm = getPaymentAccountForm(aliceClient, paymentMethodId);
// A short cut over the API:
// File emptyForm = PAYMENT_ACCOUNT_FORM.getPaymentAccountForm(paymentMethodId);
log.debug("{} Empty form saved to {}",
@ -130,7 +129,10 @@ public class AbstractPaymentAccountTest extends MethodTest {
assertEquals(paymentMethodId, emptyForm.get(PROPERTY_NAME_PAYMENT_METHOD_ID));
assertEquals("your accountname", emptyForm.get(PROPERTY_NAME_ACCOUNT_NAME));
for (String field : fields) {
assertEquals("your " + field.toLowerCase(), emptyForm.get(field));
if (field.equals("country"))
assertEquals("your two letter country code", emptyForm.get(field));
else
assertEquals("your " + field.toLowerCase(), emptyForm.get(field));
}
}
@ -153,11 +155,10 @@ public class AbstractPaymentAccountTest extends MethodTest {
assertArrayEquals(expectedTradeCurrencies.toArray(), paymentAccount.getTradeCurrencies().toArray());
}
protected final void verifyUserPayloadHasPaymentAccountWithId(String paymentAccountId) {
var getPaymentAccountsRequest = GetPaymentAccountsRequest.newBuilder().build();
var reply = grpcStubs(alicedaemon)
.paymentAccountsService.getPaymentAccounts(getPaymentAccountsRequest);
Optional<protobuf.PaymentAccount> paymentAccount = reply.getPaymentAccountsList().stream()
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());

View file

@ -17,6 +17,7 @@
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;
@ -31,6 +32,7 @@ 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;
@ -50,8 +52,12 @@ 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;
@ -65,15 +71,16 @@ import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.core.locale.CurrencyUtil.getAllAdvancedCashCurrencies;
import static bisq.core.locale.CurrencyUtil.getAllMoneyGramCurrencies;
import static bisq.core.locale.CurrencyUtil.getAllRevolutCurrencies;
import static bisq.core.locale.CurrencyUtil.getAllUpholdCurrencies;
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)
@ -99,13 +106,13 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -119,12 +126,12 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -139,14 +146,14 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -180,8 +187,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_REQUIREMENTS, "Requirements...");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
CashDepositAccount paymentAccount = (CashDepositAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
CashDepositAccount paymentAccount = (CashDepositAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
@ -198,7 +205,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -226,8 +233,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
NationalBankAccount paymentAccount = (NationalBankAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("BRL", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
@ -243,7 +250,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -259,13 +266,13 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
ChaseQuickPayAccount paymentAccount = (ChaseQuickPayAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -281,14 +288,14 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -308,8 +315,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
F2FAccount paymentAccount = (F2FAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("BRL", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
@ -317,7 +324,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -333,14 +340,14 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -354,12 +361,12 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -379,8 +386,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
@ -388,7 +395,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -414,8 +421,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
@ -425,7 +432,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -439,13 +446,13 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -465,8 +472,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_STATE, "NY");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
MoneyGramAccount paymentAccount = (MoneyGramAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
@ -474,7 +481,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_STATE), paymentAccount.getState());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -488,13 +495,13 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -510,13 +517,13 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
PopmoneyAccount paymentAccount = (PopmoneyAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -530,13 +537,13 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -550,12 +557,12 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_USERNAME, "revolut123");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
RevolutAccount paymentAccount = (RevolutAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -583,8 +590,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
SameBankAccount paymentAccount = (SameBankAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("GBP", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
@ -600,7 +607,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -620,8 +627,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BIC, "909");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
SepaInstantAccount paymentAccount = (SepaInstantAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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);
@ -631,7 +638,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBic());
// bankId == bic
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBankId());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -651,8 +658,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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);
@ -663,7 +670,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
// bankId == bic
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBankId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -694,8 +701,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
SpecificBanksAccount paymentAccount = (SpecificBanksAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
SpecificBanksAccount paymentAccount = (SpecificBanksAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("GBP", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
@ -710,7 +717,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -726,36 +733,111 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
public void testCreateTransferwiseAccount(TestInfo testInfo) {
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_EMAIL, "jan@doe.info");
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
// As per commit 88f26f93241af698ae689bf081205d0f9dc929fa
// Do not autofill all currencies by default but keep all unselected.
// verifyAccountTradeCurrencies(getAllTransferwiseCurrencies(), paymentAccount);
assertEquals(0, paymentAccount.getTradeCurrencies().size());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
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
@ -769,13 +851,13 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -791,13 +873,13 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -811,13 +893,13 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@Test
@ -839,8 +921,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
WesternUnionAccount paymentAccount = (WesternUnionAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
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());
@ -849,11 +931,18 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
print(paymentAccount);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
private void print(PaymentAccount paymentAccount) {
if (log.isDebugEnabled()) {
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
log.debug("\n{}", formatPaymentAcctTbl(singletonList(paymentAccount.toProtoMessage())));
}
}
}

View file

@ -41,7 +41,7 @@ public class GetPaymentMethodsTest extends MethodTest {
@Test
@Order(1)
public void testGetPaymentMethods() {
List<String> paymentMethodIds = getPaymentMethods(alicedaemon)
List<String> paymentMethodIds = aliceClient.getPaymentMethods()
.stream()
.map(PaymentMethod::getId)
.collect(Collectors.toList());

View file

@ -2,6 +2,8 @@ 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;
@ -22,6 +24,8 @@ public class AbstractTradeTest extends AbstractOfferTest {
// 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();
@ -30,29 +34,24 @@ public class AbstractTradeTest extends AbstractOfferTest {
protected final TradeInfo takeAlicesOffer(String offerId,
String paymentAccountId,
String takerFeeCurrencyCode) {
return bobStubs.tradesService.takeOffer(
createTakeOfferRequest(offerId,
paymentAccountId,
takerFeeCurrencyCode))
.getTrade();
return bobClient.takeOffer(offerId, paymentAccountId, takerFeeCurrencyCode);
}
@SuppressWarnings("unused")
protected final TradeInfo takeBobsOffer(String offerId,
String paymentAccountId,
String takerFeeCurrencyCode) {
return aliceStubs.tradesService.takeOffer(
createTakeOfferRequest(offerId,
paymentAccountId,
takerFeeCurrencyCode))
.getTrade();
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());
assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositPublished, trade.getIsDepositPublished());
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());

View file

@ -20,9 +20,12 @@ package bisq.apitest.method.trade;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.BtcBalanceInfo;
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;
@ -32,14 +35,14 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
import static bisq.cli.CurrencyFormat.formatSatoshis;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED;
import static bisq.core.trade.Trade.Phase.DEPOSIT_PUBLISHED;
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;
@ -47,12 +50,16 @@ import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_PAID;
import static protobuf.OpenOffer.State.AVAILABLE;
import bisq.cli.TradeFormat;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TakeBuyBTCOfferTest extends AbstractTradeTest {
// Alice is buyer, Bob is seller.
// Alice is maker/buyer, Bob is taker/seller.
// Maker and Taker fees are in BSQ.
private static final String TRADE_FEE_CURRENCY_CODE = "bsq";
@ -61,11 +68,14 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
@Order(1)
public void testTakeAlicesBuyOffer(final TestInfo testInfo) {
try {
PaymentAccount alicesUsdAccount = createDummyF2FAccount(alicedaemon, "US");
var alicesOffer = createAliceOffer(alicesUsdAccount,
"buy",
PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US");
var alicesOffer = aliceClient.createMarketBasedPricedOffer("buy",
"usd",
12500000,
12500000, // min-amount = amount
0.00,
getDefaultBuyerSecurityDepositAsPercent(),
alicesUsdAccount.getId(),
TRADE_FEE_CURRENCY_CODE);
var offerId = alicesOffer.getId();
assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc());
@ -73,10 +83,10 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
// 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 = getMyOffersSortedByDate(aliceStubs, "buy", "usd");
var alicesUsdOffers = aliceClient.getMyOffersSortedByDate("buy", "usd");
assertEquals(1, alicesUsdOffers.size());
PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobdaemon, "US");
PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US");
var trade = takeAlicesOffer(offerId, bobsUsdAccount.getId(), TRADE_FEE_CURRENCY_CODE);
assertNotNull(trade);
assertEquals(offerId, trade.getTradeId());
@ -84,24 +94,48 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
// Cache the trade id for the other tests.
tradeId = trade.getTradeId();
genBtcBlocksThenWait(1, 1000);
alicesUsdOffers = getMyOffersSortedByDate(aliceStubs, "buy", "usd");
genBtcBlocksThenWait(1, 4000);
alicesUsdOffers = aliceClient.getMyOffersSortedByDate("buy", "usd");
assertEquals(0, alicesUsdOffers.size());
trade = getTrade(bobdaemon, trade.getTradeId());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_PUBLISHED_DEPOSIT_TX)
.setPhase(DEPOSIT_PUBLISHED)
.setDepositPublished(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after taking offer and sending deposit", trade);
if (!isLongRunningTest) {
trade = bobClient.getTrade(trade.getTradeId());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_PUBLISHED_DEPOSIT_TX)
.setPhase(DEPOSIT_PUBLISHED)
.setDepositPublished(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after taking offer and sending deposit", trade);
}
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);
sleep(5000);
continue;
} else {
EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN)
.setPhase(DEPOSIT_CONFIRMED)
.setDepositConfirmed(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after deposit is confirmed", trade);
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()));
}
genBtcBlocksThenWait(1, 1000);
trade = getTrade(bobdaemon, trade.getTradeId());
EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN)
.setPhase(DEPOSIT_CONFIRMED)
.setDepositConfirmed(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after deposit is confirmed", trade);
} catch (StatusRuntimeException e) {
fail(e);
}
@ -111,17 +145,57 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
@Order(2)
public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) {
try {
var trade = getTrade(alicedaemon, tradeId);
confirmPaymentStarted(alicedaemon, trade.getTradeId());
sleep(3000);
var trade = aliceClient.getTrade(tradeId);
trade = getTrade(alicedaemon, tradeId);
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);
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);
}
@ -130,41 +204,78 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
@Test
@Order(3)
public void testBobsConfirmPaymentReceived(final TestInfo testInfo) {
var trade = getTrade(bobdaemon, tradeId);
confirmPaymentReceived(bobdaemon, trade.getTradeId());
sleep(3000);
try {
var trade = bobClient.getTrade(tradeId);
trade = getTrade(bobdaemon, 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);
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) {
genBtcBlocksThenWait(1, 1000);
try {
genBtcBlocksThenWait(1, 1000);
var trade = getTrade(alicedaemon, tradeId);
logTrade(log, testInfo, "Alice's view before keeping funds", trade);
var trade = aliceClient.getTrade(tradeId);
logTrade(log, testInfo, "Alice's view before keeping funds", trade);
keepFunds(alicedaemon, tradeId);
aliceClient.keepFunds(tradeId);
genBtcBlocksThenWait(1, 1000);
genBtcBlocksThenWait(1, 1000);
trade = getTrade(alicedaemon, 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);
BtcBalanceInfo currentBalance = getBtcBalances(bobdaemon);
log.debug("{} Alice's current available balance: {} BTC",
testName(testInfo),
formatSatoshis(currentBalance.getAvailableBalance()));
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);
BtcBalanceInfo currentBalance = aliceClient.getBtcBalances();
log.info("{} Alice's current available balance: {} BTC. Last trade:\n{}",
testName(testInfo),
formatSatoshis(currentBalance.getAvailableBalance()),
TradeFormat.format(aliceClient.getTrade(tradeId)));
} catch (StatusRuntimeException e) {
fail(e);
}
}
}

View file

@ -20,9 +20,12 @@ package bisq.apitest.method.trade;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.BtcBalanceInfo;
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;
@ -32,11 +35,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
import static bisq.cli.CurrencyFormat.formatSatoshis;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static bisq.core.trade.Trade.Phase.*;
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;
@ -44,12 +47,16 @@ import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_PAID;
import static protobuf.OpenOffer.State.AVAILABLE;
import bisq.cli.TradeFormat;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TakeSellBTCOfferTest extends AbstractTradeTest {
// Alice is seller, Bob is buyer.
// Alice is maker/seller, Bob is taker/buyer.
// Maker and Taker fees are in BTC.
private static final String TRADE_FEE_CURRENCY_CODE = "btc";
@ -60,11 +67,14 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
@Order(1)
public void testTakeAlicesSellOffer(final TestInfo testInfo) {
try {
PaymentAccount alicesUsdAccount = createDummyF2FAccount(alicedaemon, "US");
var alicesOffer = createAliceOffer(alicesUsdAccount,
"sell",
PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US");
var alicesOffer = aliceClient.createMarketBasedPricedOffer("sell",
"usd",
12500000,
12500000L,
12500000L, // min-amount = amount
0.00,
getDefaultBuyerSecurityDepositAsPercent(),
alicesUsdAccount.getId(),
TRADE_FEE_CURRENCY_CODE);
var offerId = alicesOffer.getId();
assertTrue(alicesOffer.getIsCurrencyForMakerFeeBtc());
@ -73,10 +83,10 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
// 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 = getMyOffersSortedByDate(aliceStubs, "sell", "usd");
var alicesUsdOffers = aliceClient.getMyOffersSortedByDate("sell", "usd");
assertEquals(1, alicesUsdOffers.size());
PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobdaemon, "US");
PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US");
var trade = takeAlicesOffer(offerId, bobsUsdAccount.getId(), TRADE_FEE_CURRENCY_CODE);
assertNotNull(trade);
assertEquals(offerId, trade.getTradeId());
@ -85,24 +95,47 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
tradeId = trade.getTradeId();
genBtcBlocksThenWait(1, 4000);
var takeableUsdOffers = getOffersSortedByDate(bobStubs, "sell", "usd");
var takeableUsdOffers = bobClient.getOffersSortedByDate("sell", "usd");
assertEquals(0, takeableUsdOffers.size());
trade = getTrade(bobdaemon, trade.getTradeId());
EXPECTED_PROTOCOL_STATUS.setState(BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG)
.setPhase(DEPOSIT_PUBLISHED)
.setDepositPublished(true);
verifyExpectedProtocolStatus(trade);
if (!isLongRunningTest) {
trade = bobClient.getTrade(trade.getTradeId());
EXPECTED_PROTOCOL_STATUS.setState(BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG)
.setPhase(DEPOSIT_PUBLISHED)
.setDepositPublished(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after taking offer and sending deposit", trade);
}
logTrade(log, testInfo, "Bob's view after taking offer and sending deposit", trade);
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);
sleep(5000);
continue;
} else {
EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN)
.setPhase(DEPOSIT_CONFIRMED)
.setDepositConfirmed(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after deposit is confirmed", trade);
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()));
}
genBtcBlocksThenWait(1, 1000);
trade = getTrade(bobdaemon, trade.getTradeId());
EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN)
.setPhase(DEPOSIT_CONFIRMED)
.setDepositConfirmed(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after deposit is confirmed", trade);
} catch (StatusRuntimeException e) {
fail(e);
}
@ -112,18 +145,55 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
@Order(2)
public void testBobsConfirmPaymentStarted(final TestInfo testInfo) {
try {
var trade = getTrade(bobdaemon, tradeId);
confirmPaymentStarted(bobdaemon, trade.getTradeId());
sleep(3000);
var trade = bobClient.getTrade(tradeId);
trade = getTrade(bobdaemon, tradeId);
// 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);
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);
}
@ -132,42 +202,77 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
@Test
@Order(3)
public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) {
var trade = getTrade(alicedaemon, tradeId);
confirmPaymentReceived(alicedaemon, trade.getTradeId());
sleep(3000);
try {
var trade = aliceClient.getTrade(tradeId);
trade = getTrade(alicedaemon, 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);
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) {
genBtcBlocksThenWait(1, 1000);
try {
genBtcBlocksThenWait(1, 1000);
var trade = getTrade(bobdaemon, tradeId);
logTrade(log, testInfo, "Bob's view before withdrawing funds to external wallet", trade);
var trade = bobClient.getTrade(tradeId);
logTrade(log, testInfo, "Bob's view before withdrawing funds to external wallet", trade);
String toAddress = bitcoinCli.getNewBtcAddress();
withdrawFunds(bobdaemon, tradeId, toAddress, WITHDRAWAL_TX_MEMO);
String toAddress = bitcoinCli.getNewBtcAddress();
bobClient.withdrawFunds(tradeId, toAddress, WITHDRAWAL_TX_MEMO);
genBtcBlocksThenWait(1, 1000);
genBtcBlocksThenWait(1, 1000);
trade = getTrade(bobdaemon, tradeId);
EXPECTED_PROTOCOL_STATUS.setState(WITHDRAW_COMPLETED)
.setPhase(WITHDRAWN)
.setWithdrawn(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after withdrawing funds to external wallet", trade);
BtcBalanceInfo currentBalance = getBtcBalances(bobdaemon);
log.debug("{} Bob's current available balance: {} BTC",
testName(testInfo),
formatSatoshis(currentBalance.getAvailableBalance()));
trade = bobClient.getTrade(tradeId);
EXPECTED_PROTOCOL_STATUS.setState(WITHDRAW_COMPLETED)
.setPhase(WITHDRAWN)
.setWithdrawn(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after withdrawing funds to external wallet", trade);
BtcBalanceInfo currentBalance = bobClient.getBtcBalances();
log.info("{} Bob's current available balance: {} BTC. Last trade:\n{}",
testName(testInfo),
formatSatoshis(currentBalance.getAvailableBalance()),
TradeFormat.format(bobClient.getTrade(tradeId)));
} catch (StatusRuntimeException e) {
fail(e);
}
}
}

View file

@ -37,6 +37,7 @@ import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import bisq.apitest.config.BisqAppConfig;
import bisq.apitest.method.MethodTest;
import bisq.cli.GrpcClient;
@Disabled
@Slf4j
@ -59,9 +60,7 @@ public class BsqWalletTest extends MethodTest {
@Test
@Order(1)
public void testGetUnusedBsqAddress() {
var request = createGetUnusedBsqAddressRequest();
String address = grpcStubs(alicedaemon).walletsService.getUnusedBsqAddress(request).getAddress();
var address = aliceClient.getUnusedBsqAddress();
assertFalse(address.isEmpty());
assertTrue(address.startsWith("B"));
@ -76,13 +75,13 @@ public class BsqWalletTest extends MethodTest {
@Test
@Order(2)
public void testInitialBsqBalances(final TestInfo testInfo) {
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);
BsqBalanceInfo alicesBsqBalances = aliceClient.getBsqBalances();
log.debug("{} -> Alice's BSQ Initial Balances -> \n{}",
testName(testInfo),
formatBsqBalanceInfoTbl(alicesBsqBalances));
verifyBsqBalances(ALICES_INITIAL_BSQ_BALANCES, alicesBsqBalances);
BsqBalanceInfo bobsBsqBalances = getBsqBalances(bobdaemon);
BsqBalanceInfo bobsBsqBalances = bobClient.getBsqBalances();
log.debug("{} -> Bob's BSQ Initial Balances -> \n{}",
testName(testInfo),
formatBsqBalanceInfoTbl(bobsBsqBalances));
@ -92,12 +91,12 @@ public class BsqWalletTest extends MethodTest {
@Test
@Order(3)
public void testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(final TestInfo testInfo) {
String bobsBsqAddress = getUnusedBsqAddress(bobdaemon);
sendBsq(alicedaemon, bobsBsqAddress, SEND_BSQ_AMOUNT, "100");
String bobsBsqAddress = bobClient.getUnusedBsqAddress();
aliceClient.sendBsq(bobsBsqAddress, SEND_BSQ_AMOUNT, "100");
sleep(2000);
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);
BsqBalanceInfo bobsBsqBalances = waitForNonZeroBsqUnverifiedBalance(bobdaemon);
BsqBalanceInfo alicesBsqBalances = aliceClient.getBsqBalances();
BsqBalanceInfo bobsBsqBalances = waitForNonZeroBsqUnverifiedBalance(bobClient);
log.debug("BSQ Balances Before BTC Block Gen...");
printBobAndAliceBsqBalances(testInfo,
@ -129,8 +128,8 @@ public class BsqWalletTest extends MethodTest {
// wait for both wallets to be saved to disk.
genBtcBlocksThenWait(1, 4000);
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);
BsqBalanceInfo bobsBsqBalances = waitForBsqNewAvailableConfirmedBalance(bobdaemon, 150000000);
BsqBalanceInfo alicesBsqBalances = aliceClient.getBalances().getBsq();
BsqBalanceInfo bobsBsqBalances = waitForBsqNewAvailableConfirmedBalance(bobClient, 150000000);
log.debug("See Available Confirmed BSQ Balances...");
printBobAndAliceBsqBalances(testInfo,
@ -160,26 +159,26 @@ public class BsqWalletTest extends MethodTest {
tearDownScaffold();
}
private BsqBalanceInfo waitForNonZeroBsqUnverifiedBalance(BisqAppConfig daemon) {
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 = getBsqBalances(daemon);
BsqBalanceInfo bsqBalance = grpcClient.getBsqBalances();
for (int numRequests = 1; numRequests <= 15 && bsqBalance.getUnverifiedBalance() == 0; numRequests++) {
sleep(1000);
bsqBalance = getBsqBalances(daemon);
bsqBalance = grpcClient.getBsqBalances();
}
return bsqBalance;
}
private BsqBalanceInfo waitForBsqNewAvailableConfirmedBalance(BisqAppConfig daemon,
private BsqBalanceInfo waitForBsqNewAvailableConfirmedBalance(GrpcClient grpcClient,
long staleBalance) {
BsqBalanceInfo bsqBalance = getBsqBalances(daemon);
BsqBalanceInfo bsqBalance = grpcClient.getBsqBalances();
for (int numRequests = 1;
numRequests <= 15 && bsqBalance.getAvailableConfirmedBalance() == staleBalance;
numRequests++) {
sleep(1000);
bsqBalance = getBsqBalances(daemon);
bsqBalance = grpcClient.getBsqBalances();
}
return bsqBalance;
}

View file

@ -2,6 +2,8 @@ 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;
@ -15,8 +17,11 @@ 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 bisq.common.config.BaseCurrencyNetwork.BTC_DAO_REGTEST;
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;
@ -41,7 +46,7 @@ public class BtcTxFeeRateTest extends MethodTest {
@Test
@Order(1)
public void testGetTxFeeRate(final TestInfo testInfo) {
TxFeeRateInfo txFeeRateInfo = getTxFeeRate(alicedaemon);
var txFeeRateInfo = TxFeeRateInfo.fromProto(aliceClient.getTxFeeRate());
log.debug("{} -> Fee rate with no preference: {}", testName(testInfo), txFeeRateInfo);
assertFalse(txFeeRateInfo.isUseCustomTxFeeRate());
@ -50,19 +55,30 @@ public class BtcTxFeeRateTest extends MethodTest {
@Test
@Order(2)
public void testSetTxFeeRate(final TestInfo testInfo) {
TxFeeRateInfo txFeeRateInfo = setTxFeeRate(alicedaemon, 10);
log.debug("{} -> Fee rates with custom preference: {}", testName(testInfo), txFeeRateInfo);
assertTrue(txFeeRateInfo.isUseCustomTxFeeRate());
assertEquals(10, txFeeRateInfo.getCustomTxFeeRate());
assertTrue(txFeeRateInfo.getFeeServiceRate() > 0);
public void testSetInvalidTxFeeRateShouldThrowException(final TestInfo testInfo) {
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceClient.setTxFeeRate(10));
String expectedExceptionMessage =
format("UNKNOWN: tx fee rate preference must be >= %d sats/byte",
BTC_DAO_REGTEST.getDefaultMinFeePerVbyte());
assertEquals(expectedExceptionMessage, exception.getMessage());
}
@Test
@Order(3)
public void testSetValidTxFeeRate(final TestInfo testInfo) {
var txFeeRateInfo = TxFeeRateInfo.fromProto(aliceClient.setTxFeeRate(15));
log.debug("{} -> Fee rates with custom preference: {}", testName(testInfo), txFeeRateInfo);
assertTrue(txFeeRateInfo.isUseCustomTxFeeRate());
assertEquals(15, txFeeRateInfo.getCustomTxFeeRate());
assertTrue(txFeeRateInfo.getFeeServiceRate() > 0);
}
@Test
@Order(4)
public void testUnsetTxFeeRate(final TestInfo testInfo) {
TxFeeRateInfo txFeeRateInfo = unsetTxFeeRate(alicedaemon);
var txFeeRateInfo = TxFeeRateInfo.fromProto(aliceClient.unsetTxFeeRate());
log.debug("{} -> Fee rate with no preference: {}", testName(testInfo), txFeeRateInfo);
assertFalse(txFeeRateInfo.isUseCustomTxFeeRate());

View file

@ -53,10 +53,10 @@ public class BtcWalletTest extends MethodTest {
public void testInitialBtcBalances(final TestInfo testInfo) {
// Bob & Alice's regtest Bisq wallets were initialized with 10 BTC.
BtcBalanceInfo alicesBalances = getBtcBalances(alicedaemon);
BtcBalanceInfo alicesBalances = aliceClient.getBtcBalances();
log.debug("{} Alice's BTC Balances:\n{}", testName(testInfo), formatBtcBalanceInfoTbl(alicesBalances));
BtcBalanceInfo bobsBalances = getBtcBalances(bobdaemon);
BtcBalanceInfo bobsBalances = bobClient.getBtcBalances();
log.debug("{} Bob's BTC Balances:\n{}", testName(testInfo), formatBtcBalanceInfoTbl(bobsBalances));
assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), alicesBalances.getAvailableBalance());
@ -66,20 +66,20 @@ public class BtcWalletTest extends MethodTest {
@Test
@Order(2)
public void testFundAlicesBtcWallet(final TestInfo testInfo) {
String newAddress = getUnusedBtcAddress(alicedaemon);
String newAddress = aliceClient.getUnusedBtcAddress();
bitcoinCli.sendToAddress(newAddress, "2.5");
genBtcBlocksThenWait(1, 1000);
BtcBalanceInfo btcBalanceInfo = getBtcBalances(alicedaemon);
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(getAddressBalance(alicedaemon, newAddress))));
formatAddressBalanceTbl(singletonList(aliceClient.getAddressBalance(newAddress))));
// New balance is 12.5 BTC
btcBalanceInfo = getBtcBalances(alicedaemon);
btcBalanceInfo = aliceClient.getBtcBalances();
bisq.core.api.model.BtcBalanceInfo alicesExpectedBalances =
bisq.core.api.model.BtcBalanceInfo.valueOf(1250000000,
0,
@ -94,11 +94,10 @@ public class BtcWalletTest extends MethodTest {
@Test
@Order(3)
public void testAliceSendBTCToBob(TestInfo testInfo) {
String bobsBtcAddress = getUnusedBtcAddress(bobdaemon);
String bobsBtcAddress = bobClient.getUnusedBtcAddress();
log.debug("Sending 5.5 BTC From Alice to Bob @ {}", bobsBtcAddress);
TxInfo txInfo = sendBtc(alicedaemon,
bobsBtcAddress,
TxInfo txInfo = aliceClient.sendBtc(bobsBtcAddress,
"5.50",
"100",
TX_MEMO);
@ -109,11 +108,11 @@ public class BtcWalletTest extends MethodTest {
genBtcBlocksThenWait(1, 1000);
// Fetch the tx and check for confirmation and memo.
txInfo = getTransaction(alicedaemon, txInfo.getTxId());
txInfo = aliceClient.getTransaction(txInfo.getTxId());
assertFalse(txInfo.getIsPending());
assertEquals(TX_MEMO, txInfo.getMemo());
BtcBalanceInfo alicesBalances = getBtcBalances(alicedaemon);
BtcBalanceInfo alicesBalances = aliceClient.getBtcBalances();
log.debug("{} Alice's BTC Balances:\n{}",
testName(testInfo),
formatBtcBalanceInfoTbl(alicesBalances));
@ -124,7 +123,7 @@ public class BtcWalletTest extends MethodTest {
0);
verifyBtcBalances(alicesExpectedBalances, alicesBalances);
BtcBalanceInfo bobsBalances = getBtcBalances(bobdaemon);
BtcBalanceInfo bobsBalances = bobClient.getBtcBalances();
log.debug("{} Bob's BTC Balances:\n{}",
testName(testInfo),
formatBtcBalanceInfoTbl(bobsBalances));

View file

@ -41,94 +41,83 @@ public class WalletProtectionTest extends MethodTest {
@Test
@Order(1)
public void testSetWalletPassword() {
var request = createSetWalletPasswordRequest("first-password");
grpcStubs(alicedaemon).walletsService.setWalletPassword(request);
aliceClient.setWalletPassword("first-password");
}
@Test
@Order(2)
public void testGetBalanceOnEncryptedWalletShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon));
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@Test
@Order(3)
public void testUnlockWalletFor4Seconds() {
var request = createUnlockWalletRequest("first-password", 4);
grpcStubs(alicedaemon).walletsService.unlockWallet(request);
getBtcBalances(alicedaemon); // should not throw 'wallet locked' exception
aliceClient.unlockWallet("first-password", 4);
aliceClient.getBtcBalances(); // should not throw 'wallet locked' exception
sleep(4500); // let unlock timeout expire
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon));
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@Test
@Order(4)
public void testGetBalanceAfterUnlockTimeExpiryShouldThrowException() {
var request = createUnlockWalletRequest("first-password", 3);
grpcStubs(alicedaemon).walletsService.unlockWallet(request);
aliceClient.unlockWallet("first-password", 3);
sleep(4000); // let unlock timeout expire
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon));
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@Test
@Order(5)
public void testLockWalletBeforeUnlockTimeoutExpiry() {
unlockWallet(alicedaemon, "first-password", 60);
var request = createLockWalletRequest();
grpcStubs(alicedaemon).walletsService.lockWallet(request);
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon));
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() {
var request = createLockWalletRequest();
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
grpcStubs(alicedaemon).walletsService.lockWallet(request));
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.lockWallet());
assertEquals("UNKNOWN: wallet is already locked", exception.getMessage());
}
@Test
@Order(7)
public void testUnlockWalletTimeoutOverride() {
unlockWallet(alicedaemon, "first-password", 2);
aliceClient.unlockWallet("first-password", 2);
sleep(500); // override unlock timeout after 0.5s
unlockWallet(alicedaemon, "first-password", 6);
aliceClient.unlockWallet("first-password", 6);
sleep(5000);
getBtcBalances(alicedaemon); // getbalance 5s after overriding timeout to 6s
aliceClient.getBtcBalances(); // getbalance 5s after overriding timeout to 6s
}
@Test
@Order(8)
public void testSetNewWalletPassword() {
var request = createSetWalletPasswordRequest(
"first-password", "second-password");
grpcStubs(alicedaemon).walletsService.setWalletPassword(request);
unlockWallet(alicedaemon, "second-password", 2);
getBtcBalances(alicedaemon);
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() {
var request = createSetWalletPasswordRequest(
"bad old password", "irrelevant");
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
grpcStubs(alicedaemon).walletsService.setWalletPassword(request));
aliceClient.setWalletPassword("bad old password", "irrelevant"));
assertEquals("UNKNOWN: incorrect old password", exception.getMessage());
}
@Test
@Order(10)
public void testRemoveNewWalletPassword() {
var request = createRemoveWalletPasswordRequest("second-password");
grpcStubs(alicedaemon).walletsService.removeWalletPassword(request);
getBtcBalances(alicedaemon); // should not throw 'wallet locked' exception
aliceClient.removeWalletPassword("second-password");
aliceClient.getBtcBalances(); // should not throw 'wallet locked' exception
}
@AfterAll

View file

@ -0,0 +1,100 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.condition.EnabledIf;
import static java.lang.System.getenv;
import bisq.apitest.method.trade.AbstractTradeTest;
import bisq.apitest.method.trade.TakeBuyBTCOfferTest;
import bisq.apitest.method.trade.TakeSellBTCOfferTest;
@EnabledIf("envLongRunningTestEnabled")
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class LongRunningTradesTest extends AbstractTradeTest {
@Test
@Order(1)
public void TradeLoop(final TestInfo testInfo) {
int numTrades = 0;
while (numTrades < 50) {
log.info("*******************************************************************");
log.info("Trade # {}", ++numTrades);
log.info("*******************************************************************");
EXPECTED_PROTOCOL_STATUS.init();
testTakeBuyBTCOffer(testInfo);
genBtcBlocksThenWait(1, 1000 * 15);
log.info("*******************************************************************");
log.info("Trade # {}", ++numTrades);
log.info("*******************************************************************");
EXPECTED_PROTOCOL_STATUS.init();
testTakeSellBTCOffer(testInfo);
genBtcBlocksThenWait(1, 1000 * 15);
}
}
public void testTakeBuyBTCOffer(final TestInfo testInfo) {
TakeBuyBTCOfferTest test = new TakeBuyBTCOfferTest();
setLongRunningTest(true);
test.testTakeAlicesBuyOffer(testInfo);
test.testAlicesConfirmPaymentStarted(testInfo);
test.testBobsConfirmPaymentReceived(testInfo);
test.testAlicesKeepFunds(testInfo);
}
public void testTakeSellBTCOffer(final TestInfo testInfo) {
TakeSellBTCOfferTest test = new TakeSellBTCOfferTest();
setLongRunningTest(true);
test.testTakeAlicesSellOffer(testInfo);
test.testBobsConfirmPaymentStarted(testInfo);
test.testAlicesConfirmPaymentReceived(testInfo);
test.testBobsBtcWithdrawalToExternalAddress(testInfo);
}
protected static boolean envLongRunningTestEnabled() {
String envName = "LONG_RUNNING_TRADES_TEST_ENABLED";
String envX = getenv(envName);
if (envX != null) {
log.info("Enabled, found {}.", envName);
return true;
} else {
log.info("Skipped, no environment variable {} defined.", envName);
log.info("To enable on Mac OS or Linux:"
+ "\tIf running in terminal, export LONG_RUNNING_TRADES_TEST_ENABLED=true in bash shell."
+ "\tIf running in Intellij, set LONG_RUNNING_TRADES_TEST_ENABLED=true in launcher's Environment variables field.");
return false;
}
}
}

View file

@ -42,6 +42,8 @@ public class OfferTest extends AbstractOfferTest {
public void testAmtTooLargeShouldThrowException() {
ValidateCreateOfferTest test = new ValidateCreateOfferTest();
test.testAmtTooLargeShouldThrowException();
test.testNoMatchingEURPaymentAccountShouldThrowException();
test.testNoMatchingCADPaymentAccountShouldThrowException();
}
@Test

View file

@ -13,7 +13,6 @@ 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.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
@ -26,10 +25,6 @@ import bisq.apitest.method.payment.GetPaymentMethodsTest;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class PaymentAccountTest extends AbstractPaymentAccountTest {
// Two dummy (usd +eth) accounts are set up as defaults in regtest / dao mode,
// then we add 28 more payment accounts in testCreatePaymentAccount().
private static final int EXPECTED_NUM_PAYMENT_ACCOUNTS = 2 + 28;
@BeforeAll
public static void setUp() {
try {
@ -74,13 +69,18 @@ public class PaymentAccountTest extends AbstractPaymentAccountTest {
test.testCreateSepaAccount(testInfo);
test.testCreateSpecificBanksAccount(testInfo);
test.testCreateSwishAccount(testInfo);
test.testCreateTransferwiseAccount(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);
assertEquals(EXPECTED_NUM_PAYMENT_ACCOUNTS, getPaymentAccounts(alicedaemon).size());
}
@AfterAll

View file

@ -0,0 +1,121 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.condition.EnabledIf;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.arbdaemon;
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
import static bisq.apitest.config.BisqAppConfig.seednode;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.startShutdownTimer;
import static org.junit.jupiter.api.Assertions.fail;
import bisq.apitest.config.ApiTestConfig;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.AbstractBotTest;
import bisq.apitest.scenario.bot.BotClient;
import bisq.apitest.scenario.bot.RobotBob;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
// The test case is enabled if AbstractBotTest#botScriptExists() returns true.
@EnabledIf("botScriptExists")
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ScriptedBotTest extends AbstractBotTest {
private RobotBob robotBob;
@BeforeAll
public static void startTestHarness() {
botScript = deserializeBotScript();
if (botScript.isUseTestHarness()) {
startSupportingApps(true,
true,
bitcoind,
seednode,
arbdaemon,
alicedaemon,
bobdaemon);
} else {
// We need just enough configurations to make sure Bob and testers use
// the right apiPassword, to create a bitcoin-cli helper, and RobotBob's
// gRPC stubs. But the user will have to register dispute agents before
// an offer can be taken.
config = new ApiTestConfig("--apiPassword", "xyz");
bitcoinCli = new BitcoinCliHelper(config);
log.warn("Don't forget to register dispute agents before trying to trade with me.");
}
botClient = new BotClient(bobClient);
}
@BeforeEach
public void initRobotBob() {
try {
BashScriptGenerator bashScriptGenerator = getBashScriptGenerator();
robotBob = new RobotBob(botClient, botScript, bitcoinCli, bashScriptGenerator);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void runRobotBob() {
try {
startShutdownTimer();
robotBob.run();
} catch (ManualBotShutdownException ex) {
// This exception is thrown if a /tmp/bottest-shutdown file was found.
// You can also kill -15 <pid>
// of worker.org.gradle.process.internal.worker.GradleWorkerMain 'Gradle Test Executor #'
//
// This will cleanly shut everything down as well, but you will see a
// Process 'Gradle Test Executor #' finished with non-zero exit value 143 error,
// which you may think is a test failure.
log.warn("{} Shutting down test case before test completion;"
+ " this is not a test failure.",
ex.getMessage());
} catch (Throwable throwable) {
fail(throwable);
}
}
@AfterAll
public static void tearDown() {
if (botScript.isUseTestHarness())
tearDownScaffold();
}
}

View file

@ -18,6 +18,7 @@
package bisq.apitest.scenario;
import java.io.File;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
@ -32,7 +33,7 @@ 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.apitest.method.CallRateMeteringInterceptorTest.buildInterceptorConfigFile;
import static bisq.common.file.FileUtil.deleteFileIfExists;
import static org.junit.jupiter.api.Assertions.fail;
@ -48,10 +49,12 @@ import bisq.apitest.method.RegisterDisputeAgentsTest;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class StartupTest extends MethodTest {
private static File callRateMeteringConfigFile;
@BeforeAll
public static void setUp() {
try {
File callRateMeteringConfigFile = buildInterceptorConfigFile();
callRateMeteringConfigFile = defaultRateMeterInterceptorConfig();
startSupportingApps(callRateMeteringConfigFile,
false,
false,
@ -102,6 +105,11 @@ public class StartupTest extends MethodTest {
@AfterAll
public static void tearDown() {
try {
deleteFileIfExists(callRateMeteringConfigFile);
} catch (IOException ex) {
log.error(ex.getMessage());
}
tearDownScaffold();
}
}

View file

@ -104,7 +104,8 @@ public class WalletTest extends MethodTest {
BtcTxFeeRateTest test = new BtcTxFeeRateTest();
test.testGetTxFeeRate(testInfo);
test.testSetTxFeeRate(testInfo);
test.testSetInvalidTxFeeRateShouldThrowException(testInfo);
test.testSetValidTxFeeRate(testInfo);
test.testUnsetTxFeeRate(testInfo);
}

View file

@ -0,0 +1,110 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot;
import bisq.core.locale.Country;
import protobuf.PaymentAccount;
import com.google.gson.GsonBuilder;
import java.nio.file.Paths;
import java.io.File;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.locale.CountryUtil.findCountryByCode;
import static bisq.core.payment.payload.PaymentMethod.CLEAR_X_CHANGE_ID;
import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.nio.file.Files.readAllBytes;
import bisq.apitest.method.MethodTest;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.script.BotScript;
@Slf4j
public abstract class AbstractBotTest extends MethodTest {
protected static final String BOT_SCRIPT_NAME = "bot-script.json";
protected static BotScript botScript;
protected static BotClient botClient;
protected BashScriptGenerator getBashScriptGenerator() {
if (botScript.isUseTestHarness()) {
PaymentAccount alicesAccount = createAlicesPaymentAccount();
botScript.setPaymentAccountIdForCliScripts(alicesAccount.getId());
}
return new BashScriptGenerator(config.apiPassword,
botScript.getApiPortForCliScripts(),
botScript.getPaymentAccountIdForCliScripts(),
botScript.isPrintCliScripts());
}
private PaymentAccount createAlicesPaymentAccount() {
BotPaymentAccountGenerator accountGenerator =
new BotPaymentAccountGenerator(new BotClient(aliceClient));
String paymentMethodId = botScript.getBotPaymentMethodId();
if (paymentMethodId != null) {
if (paymentMethodId.equals(CLEAR_X_CHANGE_ID)) {
// Only Zelle test accts are supported now.
return accountGenerator.createZellePaymentAccount(
"Alice's Zelle Account",
"Alice");
} else {
throw new UnsupportedOperationException(
format("This test harness bot does not work with %s payment accounts yet.",
getPaymentMethodById(paymentMethodId).getDisplayString()));
}
} else {
String countryCode = botScript.getCountryCode();
Country country = findCountryByCode(countryCode).orElseThrow(() ->
new IllegalArgumentException(countryCode + " is not a valid iso country code."));
return accountGenerator.createF2FPaymentAccount(country,
"Alice's " + country.name + " F2F Account");
}
}
protected static BotScript deserializeBotScript() {
try {
File botScriptFile = new File(getProperty("java.io.tmpdir"), BOT_SCRIPT_NAME);
String json = new String(readAllBytes(Paths.get(botScriptFile.getPath())));
return new GsonBuilder().setPrettyPrinting().create().fromJson(json, BotScript.class);
} catch (IOException ex) {
throw new IllegalStateException("Error reading script bot file contents.", ex);
}
}
@SuppressWarnings("unused") // This is used by the jupiter framework.
protected static boolean botScriptExists() {
File botScriptFile = new File(getProperty("java.io.tmpdir"), BOT_SCRIPT_NAME);
if (botScriptFile.exists()) {
botScriptFile.deleteOnExit();
log.info("Enabled, found {}.", botScriptFile.getPath());
return true;
} else {
log.info("Skipped, no bot script.\n\tTo generate a bot-script.json file, see BotScriptGenerator.");
return false;
}
}
}

View file

@ -0,0 +1,77 @@
package bisq.apitest.scenario.bot;
import bisq.core.locale.Country;
import protobuf.PaymentAccount;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.locale.CountryUtil.findCountryByCode;
import static bisq.core.payment.payload.PaymentMethod.CLEAR_X_CHANGE_ID;
import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById;
import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.MINUTES;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.script.BotScript;
@Slf4j
public
class Bot {
static final String MAKE = "MAKE";
static final String TAKE = "TAKE";
protected final BotClient botClient;
protected final BitcoinCliHelper bitcoinCli;
protected final BashScriptGenerator bashScriptGenerator;
protected final String[] actions;
protected final long protocolStepTimeLimitInMs;
protected final boolean stayAlive;
protected final boolean isUsingTestHarness;
protected final PaymentAccount paymentAccount;
public Bot(BotClient botClient,
BotScript botScript,
BitcoinCliHelper bitcoinCli,
BashScriptGenerator bashScriptGenerator) {
this.botClient = botClient;
this.bitcoinCli = bitcoinCli;
this.bashScriptGenerator = bashScriptGenerator;
this.actions = botScript.getActions();
this.protocolStepTimeLimitInMs = MINUTES.toMillis(botScript.getProtocolStepTimeLimitInMinutes());
this.stayAlive = botScript.isStayAlive();
this.isUsingTestHarness = botScript.isUseTestHarness();
if (isUsingTestHarness)
this.paymentAccount = createBotPaymentAccount(botScript);
else
this.paymentAccount = botClient.getPaymentAccount(botScript.getPaymentAccountIdForBot());
}
private PaymentAccount createBotPaymentAccount(BotScript botScript) {
BotPaymentAccountGenerator accountGenerator = new BotPaymentAccountGenerator(botClient);
String paymentMethodId = botScript.getBotPaymentMethodId();
if (paymentMethodId != null) {
if (paymentMethodId.equals(CLEAR_X_CHANGE_ID)) {
return accountGenerator.createZellePaymentAccount("Bob's Zelle Account",
"Bob");
} else {
throw new UnsupportedOperationException(
format("This bot test does not work with %s payment accounts yet.",
getPaymentMethodById(paymentMethodId).getDisplayString()));
}
} else {
Country country = findCountry(botScript.getCountryCode());
return accountGenerator.createF2FPaymentAccount(country, country.name + " F2F Account");
}
}
private Country findCountry(String countryCode) {
return findCountryByCode(countryCode).orElseThrow(() ->
new IllegalArgumentException(countryCode + " is not a valid iso country code."));
}
}

View file

@ -0,0 +1,339 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot;
import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.TradeInfo;
import protobuf.PaymentAccount;
import java.text.DecimalFormat;
import java.util.List;
import java.util.function.BiPredicate;
import lombok.extern.slf4j.Slf4j;
import static org.apache.commons.lang3.StringUtils.capitalize;
import bisq.cli.GrpcClient;
/**
* Convenience GrpcClient wrapper for bots using gRPC services.
*
* TODO Consider if the duplication smell is bad enough to force a BotClient user
* to use the GrpcClient instead (and delete this class). But right now, I think it is
* OK because moving some of the non-gRPC related methods to GrpcClient is even smellier.
*
*/
@SuppressWarnings({"JavaDoc", "unused"})
@Slf4j
public class BotClient {
private static final DecimalFormat FIXED_PRICE_FMT = new DecimalFormat("###########0");
private final GrpcClient grpcClient;
public BotClient(GrpcClient grpcClient) {
this.grpcClient = grpcClient;
}
/**
* Returns current BSQ and BTC balance information.
* @return BalancesInfo
*/
public BalancesInfo getBalance() {
return grpcClient.getBalances();
}
/**
* Return the most recent BTC market price for the given currencyCode.
* @param currencyCode
* @return double
*/
public double getCurrentBTCMarketPrice(String currencyCode) {
return grpcClient.getBtcPrice(currencyCode);
}
/**
* Return the most recent BTC market price for the given currencyCode as an integer string.
* @param currencyCode
* @return String
*/
public String getCurrentBTCMarketPriceAsIntegerString(String currencyCode) {
return FIXED_PRICE_FMT.format(getCurrentBTCMarketPrice(currencyCode));
}
/**
* Return all BUY and SELL offers for the given currencyCode.
* @param currencyCode
* @return List<OfferInfo>
*/
public List<OfferInfo> getOffers(String currencyCode) {
var buyOffers = getBuyOffers(currencyCode);
if (buyOffers.size() > 0) {
return buyOffers;
} else {
return getSellOffers(currencyCode);
}
}
/**
* Return BUY offers for the given currencyCode.
* @param currencyCode
* @return List<OfferInfo>
*/
public List<OfferInfo> getBuyOffers(String currencyCode) {
return grpcClient.getOffers("BUY", currencyCode);
}
/**
* Return SELL offers for the given currencyCode.
* @param currencyCode
* @return List<OfferInfo>
*/
public List<OfferInfo> getSellOffers(String currencyCode) {
return grpcClient.getOffers("SELL", currencyCode);
}
/**
* Create and return a new Offer using a market based price.
* @param paymentAccount
* @param direction
* @param currencyCode
* @param amountInSatoshis
* @param minAmountInSatoshis
* @param priceMarginAsPercent
* @param securityDepositAsPercent
* @param feeCurrency
* @return OfferInfo
*/
public OfferInfo createOfferAtMarketBasedPrice(PaymentAccount paymentAccount,
String direction,
String currencyCode,
long amountInSatoshis,
long minAmountInSatoshis,
double priceMarginAsPercent,
double securityDepositAsPercent,
String feeCurrency) {
return grpcClient.createMarketBasedPricedOffer(direction,
currencyCode,
amountInSatoshis,
minAmountInSatoshis,
priceMarginAsPercent,
securityDepositAsPercent,
paymentAccount.getId(),
feeCurrency);
}
/**
* Create and return a new Offer using a fixed price.
* @param paymentAccount
* @param direction
* @param currencyCode
* @param amountInSatoshis
* @param minAmountInSatoshis
* @param fixedOfferPriceAsString
* @param securityDepositAsPercent
* @param feeCurrency
* @return OfferInfo
*/
public OfferInfo createOfferAtFixedPrice(PaymentAccount paymentAccount,
String direction,
String currencyCode,
long amountInSatoshis,
long minAmountInSatoshis,
String fixedOfferPriceAsString,
double securityDepositAsPercent,
String feeCurrency) {
return grpcClient.createFixedPricedOffer(direction,
currencyCode,
amountInSatoshis,
minAmountInSatoshis,
fixedOfferPriceAsString,
securityDepositAsPercent,
paymentAccount.getId(),
feeCurrency);
}
public TradeInfo takeOffer(String offerId, PaymentAccount paymentAccount, String feeCurrency) {
return grpcClient.takeOffer(offerId, paymentAccount.getId(), feeCurrency);
}
/**
* Returns a persisted Trade with the given tradeId, or throws an exception.
* @param tradeId
* @return TradeInfo
*/
public TradeInfo getTrade(String tradeId) {
return grpcClient.getTrade(tradeId);
}
/**
* Predicate returns true if the given exception indicates the trade with the given
* tradeId exists, but the trade's contract has not been fully prepared.
*/
public final BiPredicate<Exception, String> tradeContractIsNotReady = (exception, tradeId) -> {
if (exception.getMessage().contains("no contract was found")) {
log.warn("Trade {} exists but is not fully prepared: {}.",
tradeId,
toCleanGrpcExceptionMessage(exception));
return true;
} else {
return false;
}
};
/**
* Returns a trade's contract as a Json string, or null if the trade exists
* but the contract is not ready.
* @param tradeId
* @return String
*/
public String getTradeContract(String tradeId) {
try {
var trade = grpcClient.getTrade(tradeId);
return trade.getContractAsJson();
} catch (Exception ex) {
if (tradeContractIsNotReady.test(ex, tradeId))
return null;
else
throw ex;
}
}
/**
* Returns true if the trade's taker deposit fee transaction has been published.
* @param tradeId a valid trade id
* @return boolean
*/
public boolean isTakerDepositFeeTxPublished(String tradeId) {
return grpcClient.getTrade(tradeId).getIsPayoutPublished();
}
/**
* Returns true if the trade's taker deposit fee transaction has been confirmed.
* @param tradeId a valid trade id
* @return boolean
*/
public boolean isTakerDepositFeeTxConfirmed(String tradeId) {
return grpcClient.getTrade(tradeId).getIsDepositConfirmed();
}
/**
* Returns true if the trade's 'start payment' message has been sent by the buyer.
* @param tradeId a valid trade id
* @return boolean
*/
public boolean isTradePaymentStartedSent(String tradeId) {
return grpcClient.getTrade(tradeId).getIsFiatSent();
}
/**
* Returns true if the trade's 'payment received' message has been sent by the seller.
* @param tradeId a valid trade id
* @return boolean
*/
public boolean isTradePaymentReceivedConfirmationSent(String tradeId) {
return grpcClient.getTrade(tradeId).getIsFiatReceived();
}
/**
* Returns true if the trade's payout transaction has been published.
* @param tradeId a valid trade id
* @return boolean
*/
public boolean isTradePayoutTxPublished(String tradeId) {
return grpcClient.getTrade(tradeId).getIsPayoutPublished();
}
/**
* Sends a 'confirm payment started message' for a trade with the given tradeId,
* or throws an exception.
* @param tradeId
*/
public void sendConfirmPaymentStartedMessage(String tradeId) {
grpcClient.confirmPaymentStarted(tradeId);
}
/**
* Sends a 'confirm payment received message' for a trade with the given tradeId,
* or throws an exception.
* @param tradeId
*/
public void sendConfirmPaymentReceivedMessage(String tradeId) {
grpcClient.confirmPaymentReceived(tradeId);
}
/**
* Sends a 'keep funds in wallet message' for a trade with the given tradeId,
* or throws an exception.
* @param tradeId
*/
public void sendKeepFundsMessage(String tradeId) {
grpcClient.keepFunds(tradeId);
}
/**
* Create and save a new PaymentAccount with details in the given json.
* @param json
* @return PaymentAccount
*/
public PaymentAccount createNewPaymentAccount(String json) {
return grpcClient.createPaymentAccount(json);
}
/**
* Returns a persisted PaymentAccount with the given paymentAccountId, or throws
* an exception.
* @param paymentAccountId The id of the PaymentAccount being looked up.
* @return PaymentAccount
*/
public PaymentAccount getPaymentAccount(String paymentAccountId) {
return grpcClient.getPaymentAccounts().stream()
.filter(a -> (a.getId().equals(paymentAccountId)))
.findFirst()
.orElseThrow(() ->
new PaymentAccountNotFoundException("Could not find a payment account with id "
+ paymentAccountId + "."));
}
/**
* Returns a persisted PaymentAccount with the given accountName, or throws
* an exception.
* @param accountName
* @return PaymentAccount
*/
public PaymentAccount getPaymentAccountWithName(String accountName) {
var req = GetPaymentAccountsRequest.newBuilder().build();
return grpcClient.getPaymentAccounts().stream()
.filter(a -> (a.getAccountName().equals(accountName)))
.findFirst()
.orElseThrow(() ->
new PaymentAccountNotFoundException("Could not find a payment account with name "
+ accountName + "."));
}
public String toCleanGrpcExceptionMessage(Exception ex) {
return capitalize(ex.getMessage().replaceFirst("^[A-Z_]+: ", ""));
}
}

View file

@ -0,0 +1,68 @@
package bisq.apitest.scenario.bot;
import bisq.core.api.model.PaymentAccountForm;
import bisq.core.locale.Country;
import protobuf.PaymentAccount;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.File;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.payment.payload.PaymentMethod.CLEAR_X_CHANGE_ID;
import static bisq.core.payment.payload.PaymentMethod.F2F_ID;
@Slf4j
public class BotPaymentAccountGenerator {
private final Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
private final BotClient botClient;
public BotPaymentAccountGenerator(BotClient botClient) {
this.botClient = botClient;
}
public PaymentAccount createF2FPaymentAccount(Country country, String accountName) {
try {
return botClient.getPaymentAccountWithName(accountName);
} catch (PaymentAccountNotFoundException ignored) {
// Ignore not found exception, create a new account.
}
Map<String, Object> p = getPaymentAccountFormMap(F2F_ID);
p.put("accountName", accountName);
p.put("city", country.name + " City");
p.put("country", country.code);
p.put("contact", "By Semaphore");
p.put("extraInfo", "");
// Convert the map back to a json string and create the payment account over gRPC.
return botClient.createNewPaymentAccount(gson.toJson(p));
}
public PaymentAccount createZellePaymentAccount(String accountName, String holderName) {
try {
return botClient.getPaymentAccountWithName(accountName);
} catch (PaymentAccountNotFoundException ignored) {
// Ignore not found exception, create a new account.
}
Map<String, Object> p = getPaymentAccountFormMap(CLEAR_X_CHANGE_ID);
p.put("accountName", accountName);
p.put("emailOrMobileNr", holderName + "@zelle.com");
p.put("holderName", holderName);
return botClient.createNewPaymentAccount(gson.toJson(p));
}
private Map<String, Object> getPaymentAccountFormMap(String paymentMethodId) {
PaymentAccountForm paymentAccountForm = new PaymentAccountForm();
File jsonFormTemplate = paymentAccountForm.getPaymentAccountForm(paymentMethodId);
jsonFormTemplate.deleteOnExit();
String jsonString = paymentAccountForm.toJsonString(jsonFormTemplate);
//noinspection unchecked
return (Map<String, Object>) gson.fromJson(jsonString, Object.class);
}
}

View file

@ -0,0 +1,35 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot;
import bisq.common.BisqException;
@SuppressWarnings("unused")
public class InvalidRandomOfferException extends BisqException {
public InvalidRandomOfferException(Throwable cause) {
super(cause);
}
public InvalidRandomOfferException(String format, Object... args) {
super(format, args);
}
public InvalidRandomOfferException(Throwable cause, String format, Object... args) {
super(cause, format, args);
}
}

View file

@ -0,0 +1,35 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot;
import bisq.common.BisqException;
@SuppressWarnings("unused")
public class PaymentAccountNotFoundException extends BisqException {
public PaymentAccountNotFoundException(Throwable cause) {
super(cause);
}
public PaymentAccountNotFoundException(String format, Object... args) {
super(format, args);
}
public PaymentAccountNotFoundException(Throwable cause, String format, Object... args) {
super(cause, format, args);
}
}

View file

@ -0,0 +1,177 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot;
import bisq.proto.grpc.OfferInfo;
import protobuf.PaymentAccount;
import java.security.SecureRandom;
import java.text.DecimalFormat;
import java.math.BigDecimal;
import java.util.Objects;
import java.util.function.Supplier;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static bisq.cli.CurrencyFormat.formatMarketPrice;
import static bisq.cli.CurrencyFormat.formatSatoshis;
import static bisq.common.util.MathUtils.scaleDownByPowerOf10;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static bisq.core.payment.payload.PaymentMethod.F2F_ID;
import static java.lang.String.format;
import static java.math.RoundingMode.HALF_UP;
@Slf4j
public class RandomOffer {
private static final SecureRandom RANDOM = new SecureRandom();
private static final DecimalFormat FIXED_PRICE_FMT = new DecimalFormat("###########0");
@SuppressWarnings("FieldCanBeLocal")
// If not an F2F account, keep amount <= 0.01 BTC to avoid hitting unsigned
// acct trading limit.
private final Supplier<Long> nextAmount = () ->
this.getPaymentAccount().getPaymentMethod().getId().equals(F2F_ID)
? (long) (10000000 + RANDOM.nextInt(2500000))
: (long) (750000 + RANDOM.nextInt(250000));
@SuppressWarnings("FieldCanBeLocal")
private final Supplier<Long> nextMinAmount = () -> {
boolean useMinAmount = RANDOM.nextBoolean();
if (useMinAmount) {
return this.getPaymentAccount().getPaymentMethod().getId().equals(F2F_ID)
? this.getAmount() - 5000000L
: this.getAmount() - 50000L;
} else {
return this.getAmount();
}
};
@SuppressWarnings("FieldCanBeLocal")
private final Supplier<Double> nextPriceMargin = () -> {
boolean useZeroMargin = RANDOM.nextBoolean();
if (useZeroMargin) {
return 0.00;
} else {
BigDecimal min = BigDecimal.valueOf(-5.0).setScale(2, HALF_UP);
BigDecimal max = BigDecimal.valueOf(5.0).setScale(2, HALF_UP);
BigDecimal randomBigDecimal = min.add(BigDecimal.valueOf(RANDOM.nextDouble()).multiply(max.subtract(min)));
return randomBigDecimal.setScale(2, HALF_UP).doubleValue();
}
};
private final BotClient botClient;
@Getter
private final PaymentAccount paymentAccount;
@Getter
private final String direction;
@Getter
private final String currencyCode;
@Getter
private final long amount;
@Getter
private final long minAmount;
@Getter
private final boolean useMarketBasedPrice;
@Getter
private final double priceMargin;
@Getter
private final String feeCurrency;
@Getter
private String fixedOfferPrice = "0";
@Getter
private OfferInfo offer;
@Getter
private String id;
public RandomOffer(BotClient botClient, PaymentAccount paymentAccount) {
this.botClient = botClient;
this.paymentAccount = paymentAccount;
this.direction = RANDOM.nextBoolean() ? "BUY" : "SELL";
this.currencyCode = Objects.requireNonNull(paymentAccount.getSelectedTradeCurrency()).getCode();
this.amount = nextAmount.get();
this.minAmount = nextMinAmount.get();
this.useMarketBasedPrice = RANDOM.nextBoolean();
this.priceMargin = nextPriceMargin.get();
this.feeCurrency = RANDOM.nextBoolean() ? "BSQ" : "BTC";
}
public RandomOffer create() throws InvalidRandomOfferException {
try {
printDescription();
if (useMarketBasedPrice) {
this.offer = botClient.createOfferAtMarketBasedPrice(paymentAccount,
direction,
currencyCode,
amount,
minAmount,
priceMargin,
getDefaultBuyerSecurityDepositAsPercent(),
feeCurrency);
} else {
this.offer = botClient.createOfferAtFixedPrice(paymentAccount,
direction,
currencyCode,
amount,
minAmount,
fixedOfferPrice,
getDefaultBuyerSecurityDepositAsPercent(),
feeCurrency);
}
this.id = offer.getId();
return this;
} catch (Exception ex) {
String error = format("Could not create valid %s offer for %s BTC: %s",
currencyCode,
formatSatoshis(amount),
ex.getMessage());
throw new InvalidRandomOfferException(error, ex);
}
}
private void printDescription() {
double currentMarketPrice = botClient.getCurrentBTCMarketPrice(currencyCode);
// Calculate a fixed price based on the random mkt price margin, even if we don't use it.
double differenceFromMarketPrice = currentMarketPrice * scaleDownByPowerOf10(priceMargin, 2);
double fixedOfferPriceAsDouble = direction.equals("BUY")
? currentMarketPrice - differenceFromMarketPrice
: currentMarketPrice + differenceFromMarketPrice;
this.fixedOfferPrice = FIXED_PRICE_FMT.format(fixedOfferPriceAsDouble);
String description = format("Creating new %s %s / %s offer for amount = %s BTC, min-amount = %s BTC.",
useMarketBasedPrice ? "mkt-based-price" : "fixed-priced",
direction,
currencyCode,
formatSatoshis(amount),
formatSatoshis(minAmount));
log.info(description);
if (useMarketBasedPrice) {
log.info("Offer Price Margin = {}%", priceMargin);
log.info("Expected Offer Price = {} {}", formatMarketPrice(Double.parseDouble(fixedOfferPrice)), currencyCode);
} else {
log.info("Fixed Offer Price = {} {}", fixedOfferPrice, currencyCode);
}
log.info("Current Market Price = {} {}", formatMarketPrice(currentMarketPrice), currencyCode);
}
}

View file

@ -0,0 +1,141 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.isShutdownCalled;
import static bisq.cli.TableFormat.formatBalancesTbls;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.protocol.BotProtocol;
import bisq.apitest.scenario.bot.protocol.MakerBotProtocol;
import bisq.apitest.scenario.bot.protocol.TakerBotProtocol;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.script.BotScript;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
@Slf4j
public
class RobotBob extends Bot {
@Getter
private int numTrades;
public RobotBob(BotClient botClient,
BotScript botScript,
BitcoinCliHelper bitcoinCli,
BashScriptGenerator bashScriptGenerator) {
super(botClient, botScript, bitcoinCli, bashScriptGenerator);
}
public void run() {
for (String action : actions) {
checkActionIsValid(action);
BotProtocol botProtocol;
if (action.equalsIgnoreCase(MAKE)) {
botProtocol = new MakerBotProtocol(botClient,
paymentAccount,
protocolStepTimeLimitInMs,
bitcoinCli,
bashScriptGenerator);
} else {
botProtocol = new TakerBotProtocol(botClient,
paymentAccount,
protocolStepTimeLimitInMs,
bitcoinCli,
bashScriptGenerator);
}
botProtocol.run();
if (!botProtocol.getCurrentProtocolStep().equals(DONE)) {
throw new IllegalStateException(botProtocol.getClass().getSimpleName() + " failed to complete.");
}
log.info("Completed {} successful trade{}. Current Balance:\n{}",
++numTrades,
numTrades == 1 ? "" : "s",
formatBalancesTbls(botClient.getBalance()));
if (numTrades < actions.length) {
try {
SECONDS.sleep(20);
} catch (InterruptedException ignored) {
// empty
}
}
} // end of actions loop
if (stayAlive)
waitForManualShutdown();
else
warnCLIUserBeforeShutdown();
}
private void checkActionIsValid(String action) {
if (!action.equalsIgnoreCase(MAKE) && !action.equalsIgnoreCase(TAKE))
throw new IllegalStateException(action + " is not a valid bot action; must be 'make' or 'take'");
}
private void waitForManualShutdown() {
String harnessOrCase = isUsingTestHarness ? "harness" : "case";
log.info("All script actions have been completed, but the test {} will stay alive"
+ " until a /tmp/bottest-shutdown file is detected.",
harnessOrCase);
log.info("When ready to shutdown the test {}, run '$ touch /tmp/bottest-shutdown'.",
harnessOrCase);
if (!isUsingTestHarness) {
log.warn("You will have to manually shutdown the bitcoind and Bisq nodes"
+ " running outside of the test harness.");
}
try {
while (!isShutdownCalled()) {
SECONDS.sleep(10);
}
log.warn("Manual shutdown signal received.");
} catch (ManualBotShutdownException ex) {
log.warn(ex.getMessage());
} catch (InterruptedException ignored) {
// empty
}
}
private void warnCLIUserBeforeShutdown() {
if (isUsingTestHarness) {
long delayInSeconds = 30;
log.warn("All script actions have been completed. You have {} seconds to complete any"
+ " remaining tasks before the test harness shuts down.",
delayInSeconds);
try {
SECONDS.sleep(delayInSeconds);
} catch (InterruptedException ignored) {
// empty
}
} else {
log.info("Shutting down test case");
}
}
}

View file

@ -0,0 +1,349 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot.protocol;
import bisq.proto.grpc.TradeInfo;
import protobuf.PaymentAccount;
import java.security.SecureRandom;
import java.io.File;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.*;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
import static java.lang.String.format;
import static java.lang.System.currentTimeMillis;
import static java.util.Arrays.stream;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.BotClient;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import bisq.cli.TradeFormat;
@Slf4j
public abstract class BotProtocol {
static final SecureRandom RANDOM = new SecureRandom();
static final String BUY = "BUY";
static final String SELL = "SELL";
protected final Supplier<Long> randomDelay = () -> (long) (2000 + RANDOM.nextInt(5000));
protected final AtomicLong protocolStepStartTime = new AtomicLong(0);
protected final Consumer<ProtocolStep> initProtocolStep = (step) -> {
currentProtocolStep = step;
printBotProtocolStep();
protocolStepStartTime.set(currentTimeMillis());
};
@Getter
protected ProtocolStep currentProtocolStep;
@Getter // Functions within 'this' need the @Getter.
protected final BotClient botClient;
protected final PaymentAccount paymentAccount;
protected final String currencyCode;
protected final long protocolStepTimeLimitInMs;
protected final BitcoinCliHelper bitcoinCli;
@Getter
protected final BashScriptGenerator bashScriptGenerator;
public BotProtocol(BotClient botClient,
PaymentAccount paymentAccount,
long protocolStepTimeLimitInMs,
BitcoinCliHelper bitcoinCli,
BashScriptGenerator bashScriptGenerator) {
this.botClient = botClient;
this.paymentAccount = paymentAccount;
this.currencyCode = Objects.requireNonNull(paymentAccount.getSelectedTradeCurrency()).getCode();
this.protocolStepTimeLimitInMs = protocolStepTimeLimitInMs;
this.bitcoinCli = bitcoinCli;
this.bashScriptGenerator = bashScriptGenerator;
this.currentProtocolStep = START;
}
public abstract void run();
protected boolean isWithinProtocolStepTimeLimit() {
return (currentTimeMillis() - protocolStepStartTime.get()) < protocolStepTimeLimitInMs;
}
protected void checkIsStartStep() {
if (currentProtocolStep != START) {
throw new IllegalStateException("First bot protocol step must be " + START.name());
}
}
protected void printBotProtocolStep() {
log.info("Starting protocol step {}. Bot will shutdown if step not completed within {} minutes.",
currentProtocolStep.name(), MILLISECONDS.toMinutes(protocolStepTimeLimitInMs));
if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED)) {
log.info("Generate a btc block to trigger taker's deposit fee tx confirmation.");
createGenerateBtcBlockScript();
}
}
protected final Function<TradeInfo, TradeInfo> waitForTakerFeeTxConfirm = (trade) -> {
sleep(5000);
waitForTakerFeeTxPublished(trade.getTradeId());
waitForTakerFeeTxConfirmed(trade.getTradeId());
return trade;
};
protected final Function<TradeInfo, TradeInfo> waitForPaymentStartedMessage = (trade) -> {
initProtocolStep.accept(WAIT_FOR_PAYMENT_STARTED_MESSAGE);
try {
createPaymentStartedScript(trade);
log.info(" Waiting for a 'payment started' message from buyer for trade with id {}.", trade.getTradeId());
while (isWithinProtocolStepTimeLimit()) {
checkIfShutdownCalled("Interrupted before checking if 'payment started' message has been sent.");
try {
var t = this.getBotClient().getTrade(trade.getTradeId());
if (t.getIsFiatSent()) {
log.info("Buyer has started payment for trade:\n{}", TradeFormat.format(t));
return t;
}
} catch (Exception ex) {
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex));
}
sleep(randomDelay.get());
} // end while
throw new IllegalStateException("Payment was never sent; we won't wait any longer.");
} catch (ManualBotShutdownException ex) {
throw ex; // not an error, tells bot to shutdown
} catch (Exception ex) {
throw new IllegalStateException("Error while waiting payment sent message.", ex);
}
};
protected final Function<TradeInfo, TradeInfo> sendPaymentStartedMessage = (trade) -> {
initProtocolStep.accept(SEND_PAYMENT_STARTED_MESSAGE);
checkIfShutdownCalled("Interrupted before sending 'payment started' message.");
this.getBotClient().sendConfirmPaymentStartedMessage(trade.getTradeId());
return trade;
};
protected final Function<TradeInfo, TradeInfo> waitForPaymentReceivedConfirmation = (trade) -> {
initProtocolStep.accept(WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE);
createPaymentReceivedScript(trade);
try {
log.info("Waiting for a 'payment received confirmation' message from seller for trade with id {}.", trade.getTradeId());
while (isWithinProtocolStepTimeLimit()) {
checkIfShutdownCalled("Interrupted before checking if 'payment received confirmation' message has been sent.");
try {
var t = this.getBotClient().getTrade(trade.getTradeId());
if (t.getIsFiatReceived()) {
log.info("Seller has received payment for trade:\n{}", TradeFormat.format(t));
return t;
}
} catch (Exception ex) {
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex));
}
sleep(randomDelay.get());
} // end while
throw new IllegalStateException("Payment was never received; we won't wait any longer.");
} catch (ManualBotShutdownException ex) {
throw ex; // not an error, tells bot to shutdown
} catch (Exception ex) {
throw new IllegalStateException("Error while waiting payment received confirmation message.", ex);
}
};
protected final Function<TradeInfo, TradeInfo> sendPaymentReceivedMessage = (trade) -> {
initProtocolStep.accept(SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE);
checkIfShutdownCalled("Interrupted before sending 'payment received confirmation' message.");
this.getBotClient().sendConfirmPaymentReceivedMessage(trade.getTradeId());
return trade;
};
protected final Function<TradeInfo, TradeInfo> waitForPayoutTx = (trade) -> {
initProtocolStep.accept(WAIT_FOR_PAYOUT_TX);
try {
log.info("Waiting on the 'payout tx published confirmation' for trade with id {}.", trade.getTradeId());
while (isWithinProtocolStepTimeLimit()) {
checkIfShutdownCalled("Interrupted before checking if payout tx has been published.");
try {
var t = this.getBotClient().getTrade(trade.getTradeId());
if (t.getIsPayoutPublished()) {
log.info("Payout tx {} has been published for trade:\n{}",
t.getPayoutTxId(),
TradeFormat.format(t));
return t;
}
} catch (Exception ex) {
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex));
}
sleep(randomDelay.get());
} // end while
throw new IllegalStateException("Payout tx was never published; we won't wait any longer.");
} catch (ManualBotShutdownException ex) {
throw ex; // not an error, tells bot to shutdown
} catch (Exception ex) {
throw new IllegalStateException("Error while waiting for published payout tx.", ex);
}
};
protected final Function<TradeInfo, TradeInfo> keepFundsFromTrade = (trade) -> {
initProtocolStep.accept(KEEP_FUNDS);
var isBuy = trade.getOffer().getDirection().equalsIgnoreCase(BUY);
var isSell = trade.getOffer().getDirection().equalsIgnoreCase(SELL);
var cliUserIsSeller = (this instanceof MakerBotProtocol && isBuy) || (this instanceof TakerBotProtocol && isSell);
if (cliUserIsSeller) {
createKeepFundsScript(trade);
} else {
createGetBalanceScript();
}
checkIfShutdownCalled("Interrupted before closing trade with 'keep funds' command.");
this.getBotClient().sendKeepFundsMessage(trade.getTradeId());
return trade;
};
protected void createPaymentStartedScript(TradeInfo trade) {
File script = bashScriptGenerator.createPaymentStartedScript(trade);
printCliHintAndOrScript(script, "The manual CLI side can send a 'payment started' message");
}
protected void createPaymentReceivedScript(TradeInfo trade) {
File script = bashScriptGenerator.createPaymentReceivedScript(trade);
printCliHintAndOrScript(script, "The manual CLI side can sent a 'payment received confirmation' message");
}
protected void createKeepFundsScript(TradeInfo trade) {
File script = bashScriptGenerator.createKeepFundsScript(trade);
printCliHintAndOrScript(script, "The manual CLI side can close the trade");
}
protected void createGetBalanceScript() {
File script = bashScriptGenerator.createGetBalanceScript();
printCliHintAndOrScript(script, "The manual CLI side can view current balances");
}
protected void createGenerateBtcBlockScript() {
String newBitcoinCoreAddress = bitcoinCli.getNewBtcAddress();
File script = bashScriptGenerator.createGenerateBtcBlockScript(newBitcoinCoreAddress);
printCliHintAndOrScript(script, "The manual CLI side can generate 1 btc block");
}
protected void printCliHintAndOrScript(File script, String hint) {
log.info("{} by running bash script '{}'.", hint, script.getAbsolutePath());
if (this.getBashScriptGenerator().isPrintCliScripts())
this.getBashScriptGenerator().printCliScript(script, log);
sleep(5000); // Allow 5s for CLI user to read the hint.
}
protected void sleep(long ms) {
try {
MILLISECONDS.sleep(ms);
} catch (InterruptedException ignored) {
// empty
}
}
private void waitForTakerFeeTxPublished(String tradeId) {
waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED);
}
private void waitForTakerFeeTxConfirmed(String tradeId) {
waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED);
}
private void waitForTakerDepositFee(String tradeId, ProtocolStep depositTxProtocolStep) {
initProtocolStep.accept(depositTxProtocolStep);
validateCurrentProtocolStep(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED);
try {
log.info(waitingForDepositFeeTxMsg(tradeId));
while (isWithinProtocolStepTimeLimit()) {
checkIfShutdownCalled("Interrupted before checking taker deposit fee tx is published and confirmed.");
try {
var trade = this.getBotClient().getTrade(tradeId);
if (isDepositFeeTxStepComplete.test(trade))
return;
else
sleep(randomDelay.get());
} catch (Exception ex) {
if (this.getBotClient().tradeContractIsNotReady.test(ex, tradeId))
sleep(randomDelay.get());
else
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex));
}
} // end while
throw new IllegalStateException(stoppedWaitingForDepositFeeTxMsg(this.getBotClient().getTrade(tradeId).getDepositTxId()));
} catch (ManualBotShutdownException ex) {
throw ex; // not an error, tells bot to shutdown
} catch (Exception ex) {
throw new IllegalStateException("Error while waiting for taker deposit tx to be published or confirmed.", ex);
}
}
private final Predicate<TradeInfo> isDepositFeeTxStepComplete = (trade) -> {
if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) && trade.getIsDepositPublished()) {
log.info("Taker deposit fee tx {} has been published.", trade.getDepositTxId());
return true;
} else if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED) && trade.getIsDepositConfirmed()) {
log.info("Taker deposit fee tx {} has been confirmed.", trade.getDepositTxId());
return true;
} else {
return false;
}
};
private void validateCurrentProtocolStep(Enum<?>... validBotSteps) {
for (Enum<?> validBotStep : validBotSteps) {
if (currentProtocolStep.equals(validBotStep))
return;
}
throw new IllegalStateException("Unexpected bot step: " + currentProtocolStep.name() + ".\n"
+ "Must be one of "
+ stream(validBotSteps).map((Enum::name)).collect(Collectors.joining(","))
+ ".");
}
private String waitingForDepositFeeTxMsg(String tradeId) {
return format("Waiting for taker deposit fee tx for trade %s to be %s.",
tradeId,
currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) ? "published" : "confirmed");
}
private String stoppedWaitingForDepositFeeTxMsg(String txId) {
return format("Taker deposit fee tx %s is took too long to be %s; we won't wait any longer.",
txId,
currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) ? "published" : "confirmed");
}
}

View file

@ -0,0 +1,114 @@
package bisq.apitest.scenario.bot.protocol;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.TradeInfo;
import protobuf.PaymentAccount;
import java.io.File;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_OFFER_TAKER;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
import static bisq.cli.TableFormat.formatOfferTable;
import static java.util.Collections.singletonList;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.BotClient;
import bisq.apitest.scenario.bot.RandomOffer;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import bisq.cli.TradeFormat;
@Slf4j
public class MakerBotProtocol extends BotProtocol {
public MakerBotProtocol(BotClient botClient,
PaymentAccount paymentAccount,
long protocolStepTimeLimitInMs,
BitcoinCliHelper bitcoinCli,
BashScriptGenerator bashScriptGenerator) {
super(botClient,
paymentAccount,
protocolStepTimeLimitInMs,
bitcoinCli,
bashScriptGenerator);
}
@Override
public void run() {
checkIsStartStep();
Function<Supplier<OfferInfo>, TradeInfo> makeTrade = waitForNewTrade.andThen(waitForTakerFeeTxConfirm);
var trade = makeTrade.apply(randomOffer);
var makerIsBuyer = trade.getOffer().getDirection().equalsIgnoreCase(BUY);
Function<TradeInfo, TradeInfo> completeFiatTransaction = makerIsBuyer
? sendPaymentStartedMessage.andThen(waitForPaymentReceivedConfirmation)
: waitForPaymentStartedMessage.andThen(sendPaymentReceivedMessage);
completeFiatTransaction.apply(trade);
Function<TradeInfo, TradeInfo> closeTrade = waitForPayoutTx.andThen(keepFundsFromTrade);
closeTrade.apply(trade);
currentProtocolStep = DONE;
}
private final Supplier<OfferInfo> randomOffer = () -> {
checkIfShutdownCalled("Interrupted before creating random offer.");
OfferInfo offer = new RandomOffer(botClient, paymentAccount).create().getOffer();
log.info("Created random {} offer\n{}", currencyCode, formatOfferTable(singletonList(offer), currencyCode));
return offer;
};
private final Function<Supplier<OfferInfo>, TradeInfo> waitForNewTrade = (randomOffer) -> {
initProtocolStep.accept(WAIT_FOR_OFFER_TAKER);
OfferInfo offer = randomOffer.get();
createTakeOfferCliScript(offer);
try {
log.info("Impatiently waiting for offer {} to be taken, repeatedly calling gettrade.", offer.getId());
while (isWithinProtocolStepTimeLimit()) {
checkIfShutdownCalled("Interrupted while waiting for offer to be taken.");
try {
var trade = getNewTrade(offer.getId());
if (trade.isPresent())
return trade.get();
else
sleep(randomDelay.get());
} catch (Exception ex) {
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex), ex);
}
} // end while
throw new IllegalStateException("Offer was never taken; we won't wait any longer.");
} catch (ManualBotShutdownException ex) {
throw ex; // not an error, tells bot to shutdown
} catch (Exception ex) {
throw new IllegalStateException("Error while waiting for offer to be taken.", ex);
}
};
private Optional<TradeInfo> getNewTrade(String offerId) {
try {
var trade = botClient.getTrade(offerId);
log.info("Offer {} was taken, new trade:\n{}", offerId, TradeFormat.format(trade));
return Optional.of(trade);
} catch (Exception ex) {
// Get trade will throw a non-fatal gRPC exception if not found.
log.info(this.getBotClient().toCleanGrpcExceptionMessage(ex));
return Optional.empty();
}
}
private void createTakeOfferCliScript(OfferInfo offer) {
File script = bashScriptGenerator.createTakeOfferScript(offer);
printCliHintAndOrScript(script, "The manual CLI side can take the offer");
}
}

View file

@ -0,0 +1,17 @@
package bisq.apitest.scenario.bot.protocol;
public enum ProtocolStep {
START,
FIND_OFFER,
TAKE_OFFER,
WAIT_FOR_OFFER_TAKER,
WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED,
WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED,
SEND_PAYMENT_STARTED_MESSAGE,
WAIT_FOR_PAYMENT_STARTED_MESSAGE,
SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE,
WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE,
WAIT_FOR_PAYOUT_TX,
KEEP_FUNDS,
DONE
}

View file

@ -0,0 +1,136 @@
package bisq.apitest.scenario.bot.protocol;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.TradeInfo;
import protobuf.PaymentAccount;
import java.io.File;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.FIND_OFFER;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.TAKE_OFFER;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
import static bisq.cli.TableFormat.formatOfferTable;
import static bisq.core.payment.payload.PaymentMethod.F2F_ID;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.BotClient;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
@Slf4j
public class TakerBotProtocol extends BotProtocol {
public TakerBotProtocol(BotClient botClient,
PaymentAccount paymentAccount,
long protocolStepTimeLimitInMs,
BitcoinCliHelper bitcoinCli,
BashScriptGenerator bashScriptGenerator) {
super(botClient,
paymentAccount,
protocolStepTimeLimitInMs,
bitcoinCli,
bashScriptGenerator);
}
@Override
public void run() {
checkIsStartStep();
Function<OfferInfo, TradeInfo> takeTrade = takeOffer.andThen(waitForTakerFeeTxConfirm);
var trade = takeTrade.apply(findOffer.get());
var takerIsSeller = trade.getOffer().getDirection().equalsIgnoreCase(BUY);
Function<TradeInfo, TradeInfo> completeFiatTransaction = takerIsSeller
? waitForPaymentStartedMessage.andThen(sendPaymentReceivedMessage)
: sendPaymentStartedMessage.andThen(waitForPaymentReceivedConfirmation);
completeFiatTransaction.apply(trade);
Function<TradeInfo, TradeInfo> closeTrade = waitForPayoutTx.andThen(keepFundsFromTrade);
closeTrade.apply(trade);
currentProtocolStep = DONE;
}
private final Supplier<Optional<OfferInfo>> firstOffer = () -> {
var offers = botClient.getOffers(currencyCode);
if (offers.size() > 0) {
log.info("Offers found:\n{}", formatOfferTable(offers, currencyCode));
OfferInfo offer = offers.get(0);
log.info("Will take first offer {}", offer.getId());
return Optional.of(offer);
} else {
log.info("No buy or sell {} offers found.", currencyCode);
return Optional.empty();
}
};
private final Supplier<OfferInfo> findOffer = () -> {
initProtocolStep.accept(FIND_OFFER);
createMakeOfferScript();
try {
log.info("Impatiently waiting for at least one {} offer to be created, repeatedly calling getoffers.", currencyCode);
while (isWithinProtocolStepTimeLimit()) {
checkIfShutdownCalled("Interrupted while checking offers.");
try {
Optional<OfferInfo> offer = firstOffer.get();
if (offer.isPresent())
return offer.get();
else
sleep(randomDelay.get());
} catch (Exception ex) {
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex), ex);
}
} // end while
throw new IllegalStateException("Offer was never created; we won't wait any longer.");
} catch (ManualBotShutdownException ex) {
throw ex; // not an error, tells bot to shutdown
} catch (Exception ex) {
throw new IllegalStateException("Error while waiting for a new offer.", ex);
}
};
private final Function<OfferInfo, TradeInfo> takeOffer = (offer) -> {
initProtocolStep.accept(TAKE_OFFER);
checkIfShutdownCalled("Interrupted before taking offer.");
String feeCurrency = RANDOM.nextBoolean() ? "BSQ" : "BTC";
return botClient.takeOffer(offer.getId(), paymentAccount, feeCurrency);
};
private void createMakeOfferScript() {
String direction = RANDOM.nextBoolean() ? "BUY" : "SELL";
String feeCurrency = RANDOM.nextBoolean() ? "BSQ" : "BTC";
boolean createMarginPricedOffer = RANDOM.nextBoolean();
// If not using an F2F account, don't go over possible 0.01 BTC
// limit if account is not signed.
String amount = paymentAccount.getPaymentMethod().getId().equals(F2F_ID)
? "0.25"
: "0.01";
File script;
if (createMarginPricedOffer) {
script = bashScriptGenerator.createMakeMarginPricedOfferScript(direction,
currencyCode,
amount,
"0.0",
"15.0",
feeCurrency);
} else {
script = bashScriptGenerator.createMakeFixedPricedOfferScript(direction,
currencyCode,
amount,
botClient.getCurrentBTCMarketPriceAsIntegerString(currencyCode),
"15.0",
feeCurrency);
}
printCliHintAndOrScript(script, "The manual CLI side can create an offer");
}
}

View file

@ -0,0 +1,235 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot.script;
import bisq.common.file.FileUtil;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.TradeInfo;
import com.google.common.io.Files;
import java.nio.file.Paths;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.io.FileWriteMode.APPEND;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.readAllBytes;
@Slf4j
@Getter
public class BashScriptGenerator {
private final int apiPort;
private final String apiPassword;
private final String paymentAccountId;
private final String cliBase;
private final boolean printCliScripts;
public BashScriptGenerator(String apiPassword,
int apiPort,
String paymentAccountId,
boolean printCliScripts) {
this.apiPassword = apiPassword;
this.apiPort = apiPort;
this.paymentAccountId = paymentAccountId;
this.printCliScripts = printCliScripts;
this.cliBase = format("./bisq-cli --password=%s --port=%d", apiPassword, apiPort);
}
public File createMakeMarginPricedOfferScript(String direction,
String currencyCode,
String amount,
String marketPriceMargin,
String securityDeposit,
String feeCurrency) {
String makeOfferCmd = format("%s createoffer --payment-account=%s "
+ " --direction=%s"
+ " --currency-code=%s"
+ " --amount=%s"
+ " --market-price-margin=%s"
+ " --security-deposit=%s"
+ " --fee-currency=%s",
cliBase,
this.getPaymentAccountId(),
direction,
currencyCode,
amount,
marketPriceMargin,
securityDeposit,
feeCurrency);
String getOffersCmd = format("%s getmyoffers --direction=%s --currency-code=%s",
cliBase,
direction,
currencyCode);
return createCliScript("createoffer.sh",
makeOfferCmd,
"sleep 2",
getOffersCmd);
}
public File createMakeFixedPricedOfferScript(String direction,
String currencyCode,
String amount,
String fixedPrice,
String securityDeposit,
String feeCurrency) {
String makeOfferCmd = format("%s createoffer --payment-account=%s "
+ " --direction=%s"
+ " --currency-code=%s"
+ " --amount=%s"
+ " --fixed-price=%s"
+ " --security-deposit=%s"
+ " --fee-currency=%s",
cliBase,
this.getPaymentAccountId(),
direction,
currencyCode,
amount,
fixedPrice,
securityDeposit,
feeCurrency);
String getOffersCmd = format("%s getmyoffers --direction=%s --currency-code=%s",
cliBase,
direction,
currencyCode);
return createCliScript("createoffer.sh",
makeOfferCmd,
"sleep 2",
getOffersCmd);
}
public File createTakeOfferScript(OfferInfo offer) {
String getOffersCmd = format("%s getoffers --direction=%s --currency-code=%s",
cliBase,
offer.getDirection(),
offer.getCounterCurrencyCode());
String takeOfferCmd = format("%s takeoffer --offer-id=%s --payment-account=%s --fee-currency=BSQ",
cliBase,
offer.getId(),
this.getPaymentAccountId());
String getTradeCmd = format("%s gettrade --trade-id=%s",
cliBase,
offer.getId());
return createCliScript("takeoffer.sh",
getOffersCmd,
takeOfferCmd,
"sleep 5",
getTradeCmd);
}
public File createPaymentStartedScript(TradeInfo trade) {
String paymentStartedCmd = format("%s confirmpaymentstarted --trade-id=%s",
cliBase,
trade.getTradeId());
String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId());
return createCliScript("confirmpaymentstarted.sh",
paymentStartedCmd,
"sleep 2",
getTradeCmd);
}
public File createPaymentReceivedScript(TradeInfo trade) {
String paymentStartedCmd = format("%s confirmpaymentreceived --trade-id=%s",
cliBase,
trade.getTradeId());
String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId());
return createCliScript("confirmpaymentreceived.sh",
paymentStartedCmd,
"sleep 2",
getTradeCmd);
}
public File createKeepFundsScript(TradeInfo trade) {
String paymentStartedCmd = format("%s keepfunds --trade-id=%s", cliBase, trade.getTradeId());
String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId());
String getBalanceCmd = format("%s getbalance", cliBase);
return createCliScript("keepfunds.sh",
paymentStartedCmd,
"sleep 2",
getTradeCmd,
getBalanceCmd);
}
public File createGetBalanceScript() {
String getBalanceCmd = format("%s getbalance", cliBase);
return createCliScript("getbalance.sh", getBalanceCmd);
}
public File createGenerateBtcBlockScript(String address) {
String bitcoinCliCmd = format("bitcoin-cli -regtest -rpcport=19443 -rpcuser=apitest"
+ " -rpcpassword=apitest generatetoaddress 1 \"%s\"",
address);
return createCliScript("genbtcblk.sh",
bitcoinCliCmd);
}
public File createCliScript(String scriptName, String... commands) {
String filename = getProperty("java.io.tmpdir") + File.separator + scriptName;
File oldScript = new File(filename);
if (oldScript.exists()) {
try {
FileUtil.deleteFileIfExists(oldScript);
} catch (IOException ex) {
throw new IllegalStateException("Unable to delete old script.", ex);
}
}
File script = new File(filename);
try {
List<CharSequence> lines = new ArrayList<>();
lines.add("#!/bin/bash");
lines.add("############################################################");
lines.add("# This example CLI script may be overwritten during the test");
lines.add("# run, and will be deleted when the test harness shuts down.");
lines.add("# Make a copy if you want to save it.");
lines.add("############################################################");
lines.add("set -x");
Collections.addAll(lines, commands);
Files.asCharSink(script, UTF_8, APPEND).writeLines(lines);
if (!script.setExecutable(true))
throw new IllegalStateException("Unable to set script owner's execute permission.");
} catch (IOException ex) {
log.error("", ex);
throw new IllegalStateException(ex);
} finally {
script.deleteOnExit();
}
return script;
}
public void printCliScript(File cliScript,
org.slf4j.Logger logger) {
try {
String contents = new String(readAllBytes(Paths.get(cliScript.getPath())));
logger.info("CLI script {}:\n{}", cliScript.getAbsolutePath(), contents);
} catch (IOException ex) {
throw new IllegalStateException("Error reading CLI script contents.", ex);
}
}
}

View file

@ -0,0 +1,78 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot.script;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.annotation.Nullable;
@Getter
@ToString
public
class BotScript {
// Common, default is true.
private final boolean useTestHarness;
// Used only with test harness. Mutually exclusive, but if both are not null,
// the botPaymentMethodId takes precedence over countryCode.
@Nullable
private final String botPaymentMethodId;
@Nullable
private final String countryCode;
// Used only without test harness.
@Nullable
@Setter
private String paymentAccountIdForBot;
@Nullable
@Setter
private String paymentAccountIdForCliScripts;
// Common, used with or without test harness.
private final int apiPortForCliScripts;
private final String[] actions;
private final long protocolStepTimeLimitInMinutes;
private final boolean printCliScripts;
private final boolean stayAlive;
@SuppressWarnings("NullableProblems")
BotScript(boolean useTestHarness,
String botPaymentMethodId,
String countryCode,
String paymentAccountIdForBot,
String paymentAccountIdForCliScripts,
String[] actions,
int apiPortForCliScripts,
long protocolStepTimeLimitInMinutes,
boolean printCliScripts,
boolean stayAlive) {
this.useTestHarness = useTestHarness;
this.botPaymentMethodId = botPaymentMethodId;
this.countryCode = countryCode != null ? countryCode.toUpperCase() : null;
this.paymentAccountIdForBot = paymentAccountIdForBot;
this.paymentAccountIdForCliScripts = paymentAccountIdForCliScripts;
this.apiPortForCliScripts = apiPortForCliScripts;
this.actions = actions;
this.protocolStepTimeLimitInMinutes = protocolStepTimeLimitInMinutes;
this.printCliScripts = printCliScripts;
this.stayAlive = stayAlive;
}
}

View file

@ -0,0 +1,247 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot.script;
import bisq.common.file.JsonFileManager;
import bisq.common.util.Utilities;
import joptsimple.BuiltinHelpFormatter;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static java.lang.System.err;
import static java.lang.System.exit;
import static java.lang.System.getProperty;
import static java.lang.System.out;
@Slf4j
public class BotScriptGenerator {
private final boolean useTestHarness;
@Nullable
private final String countryCode;
@Nullable
private final String botPaymentMethodId;
@Nullable
private final String paymentAccountIdForBot;
@Nullable
private final String paymentAccountIdForCliScripts;
private final int apiPortForCliScripts;
private final String actions;
private final int protocolStepTimeLimitInMinutes;
private final boolean printCliScripts;
private final boolean stayAlive;
public BotScriptGenerator(String[] args) {
OptionParser parser = new OptionParser();
var helpOpt = parser.accepts("help", "Print this help text.")
.forHelp();
OptionSpec<Boolean> useTestHarnessOpt = parser
.accepts("use-testharness", "Use the test harness, or manually start your own nodes.")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(true);
OptionSpec<String> actionsOpt = parser
.accepts("actions", "A comma delimited list with no spaces, e.g., make,take,take,make,...")
.withRequiredArg();
OptionSpec<String> botPaymentMethodIdOpt = parser
.accepts("bot-payment-method",
"The bot's (Bob) payment method id. If using the test harness,"
+ " the id will be used to automatically create a payment account.")
.withRequiredArg();
OptionSpec<String> countryCodeOpt = parser
.accepts("country-code",
"The two letter country-code for an F2F payment account if using the test harness,"
+ " but the bot-payment-method option takes precedence.")
.withRequiredArg();
OptionSpec<Integer> apiPortForCliScriptsOpt = parser
.accepts("api-port-for-cli-scripts",
"The api port used in bot generated bash/cli scripts.")
.withRequiredArg()
.ofType(Integer.class)
.defaultsTo(9998);
OptionSpec<String> paymentAccountIdForBotOpt = parser
.accepts("payment-account-for-bot",
"The bot side's payment account id, when the test harness is not used,"
+ " and Bob & Alice accounts are not automatically created.")
.withRequiredArg();
OptionSpec<String> paymentAccountIdForCliScriptsOpt = parser
.accepts("payment-account-for-cli-scripts",
"The other side's payment account id, used in generated bash/cli scripts when"
+ " the test harness is not used, and Bob & Alice accounts are not automatically created.")
.withRequiredArg();
OptionSpec<Integer> protocolStepTimeLimitInMinutesOpt = parser
.accepts("step-time-limit", "Each protocol step's time limit in minutes")
.withRequiredArg()
.ofType(Integer.class)
.defaultsTo(60);
OptionSpec<Boolean> printCliScriptsOpt = parser
.accepts("print-cli-scripts", "Print the generated CLI scripts from bot")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false);
OptionSpec<Boolean> stayAliveOpt = parser
.accepts("stay-alive", "Leave test harness nodes running after the last action.")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(true);
OptionSet options = parser.parse(args);
if (options.has(helpOpt)) {
printHelp(parser, out);
exit(0);
}
if (!options.has(actionsOpt)) {
printHelp(parser, err);
exit(1);
}
this.useTestHarness = options.has(useTestHarnessOpt) ? options.valueOf(useTestHarnessOpt) : true;
this.actions = options.valueOf(actionsOpt);
this.apiPortForCliScripts = options.has(apiPortForCliScriptsOpt) ? options.valueOf(apiPortForCliScriptsOpt) : 9998;
this.botPaymentMethodId = options.has(botPaymentMethodIdOpt) ? options.valueOf(botPaymentMethodIdOpt) : null;
this.countryCode = options.has(countryCodeOpt) ? options.valueOf(countryCodeOpt) : null;
this.paymentAccountIdForBot = options.has(paymentAccountIdForBotOpt) ? options.valueOf(paymentAccountIdForBotOpt) : null;
this.paymentAccountIdForCliScripts = options.has(paymentAccountIdForCliScriptsOpt) ? options.valueOf(paymentAccountIdForCliScriptsOpt) : null;
this.protocolStepTimeLimitInMinutes = options.valueOf(protocolStepTimeLimitInMinutesOpt);
this.printCliScripts = options.valueOf(printCliScriptsOpt);
this.stayAlive = options.valueOf(stayAliveOpt);
var noPaymentAccountCountryOrMethodForTestHarness = useTestHarness &&
(!options.has(countryCodeOpt) && !options.has(botPaymentMethodIdOpt));
if (noPaymentAccountCountryOrMethodForTestHarness) {
log.error("When running the test harness, payment accounts are automatically generated,");
log.error("and you must provide one of the following options:");
log.error(" \t\t(1) --bot-payment-method=<payment-method-id> OR");
log.error(" \t\t(2) --country-code=<country-code>");
log.error("If the bot-payment-method option is not present, the bot will create"
+ " a country based F2F account using the country-code.");
log.error("If both are present, the bot-payment-method will take precedence. "
+ "Currently, only the CLEAR_X_CHANGE_ID bot-payment-method is supported.");
printHelp(parser, err);
exit(1);
}
var noPaymentAccountIdOrApiPortForCliScripts = !useTestHarness &&
(!options.has(paymentAccountIdForCliScriptsOpt) || !options.has(paymentAccountIdForBotOpt));
if (noPaymentAccountIdOrApiPortForCliScripts) {
log.error("If not running the test harness, payment accounts are not automatically generated,");
log.error("and you must provide three options:");
log.error(" \t\t(1) --api-port-for-cli-scripts=<port>");
log.error(" \t\t(2) --payment-account-for-bot=<payment-account-id>");
log.error(" \t\t(3) --payment-account-for-cli-scripts=<payment-account-id>");
log.error("These will be used by the bot and in CLI scripts the bot will generate when creating an offer.");
printHelp(parser, err);
exit(1);
}
}
private void printHelp(OptionParser parser, PrintStream stream) {
try {
String usage = "Examples\n--------\n"
+ examplesUsingTestHarness()
+ examplesNotUsingTestHarness();
stream.println();
parser.formatHelpWith(new HelpFormatter());
parser.printHelpOn(stream);
stream.println();
stream.println(usage);
stream.println();
} catch (IOException ex) {
log.error("", ex);
}
}
private String examplesUsingTestHarness() {
@SuppressWarnings("StringBufferReplaceableByString") StringBuilder builder = new StringBuilder();
builder.append("To generate a bot-script.json file that will start the test harness,");
builder.append(" create F2F accounts for Bob and Alice,");
builder.append(" and take an offer created by Alice's CLI:").append("\n");
builder.append("\tUsage: BotScriptGenerator").append("\n");
builder.append("\t\t").append("--use-testharness=true").append("\n");
builder.append("\t\t").append("--country-code=<country-code>").append("\n");
builder.append("\t\t").append("--actions=take").append("\n");
builder.append("\n");
builder.append("To generate a bot-script.json file that will start the test harness,");
builder.append(" create Zelle accounts for Bob and Alice,");
builder.append(" and create an offer to be taken by Alice's CLI:").append("\n");
builder.append("\tUsage: BotScriptGenerator").append("\n");
builder.append("\t\t").append("--use-testharness=true").append("\n");
builder.append("\t\t").append("--bot-payment-method=CLEAR_X_CHANGE").append("\n");
builder.append("\t\t").append("--actions=make").append("\n");
builder.append("\n");
return builder.toString();
}
private String examplesNotUsingTestHarness() {
@SuppressWarnings("StringBufferReplaceableByString") StringBuilder builder = new StringBuilder();
builder.append("To generate a bot-script.json file that will not start the test harness,");
builder.append(" but will create useful bash scripts for the CLI user,");
builder.append(" and make two offers, then take two offers:").append("\n");
builder.append("\tUsage: BotScriptGenerator").append("\n");
builder.append("\t\t").append("--use-testharness=false").append("\n");
builder.append("\t\t").append("--api-port-for-cli-scripts=<port>").append("\n");
builder.append("\t\t").append("--payment-account-for-bot=<payment-account-id>").append("\n");
builder.append("\t\t").append("--payment-account-for-cli-scripts=<payment-account-id>").append("\n");
builder.append("\t\t").append("--actions=make,make,take,take").append("\n");
builder.append("\n");
return builder.toString();
}
private String generateBotScriptTemplate() {
return Utilities.objectToJson(new BotScript(
useTestHarness,
botPaymentMethodId,
countryCode,
paymentAccountIdForBot,
paymentAccountIdForCliScripts,
actions.split("\\s*,\\s*").clone(),
apiPortForCliScripts,
protocolStepTimeLimitInMinutes,
printCliScripts,
stayAlive));
}
public static void main(String[] args) {
BotScriptGenerator generator = new BotScriptGenerator(args);
String json = generator.generateBotScriptTemplate();
String destDir = getProperty("java.io.tmpdir");
JsonFileManager jsonFileManager = new JsonFileManager(new File(destDir));
jsonFileManager.writeToDisc(json, "bot-script");
JsonFileManager.shutDownAllInstances();
log.info("Saved {}/bot-script.json", destDir);
log.info("bot-script.json contents\n{}", json);
}
// Makes a formatter with a given overall row width of 120 and column separator width of 2.
private static class HelpFormatter extends BuiltinHelpFormatter {
public HelpFormatter() {
super(120, 2);
}
}
}

View file

@ -0,0 +1,35 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot.shutdown;
import bisq.common.BisqException;
@SuppressWarnings("unused")
public class ManualBotShutdownException extends BisqException {
public ManualBotShutdownException(Throwable cause) {
super(cause);
}
public ManualBotShutdownException(String format, Object... args) {
super(format, args);
}
public ManualBotShutdownException(Throwable cause, String format, Object... args) {
super(cause, format, args);
}
}

View file

@ -0,0 +1,64 @@
package bisq.apitest.scenario.bot.shutdown;
import bisq.common.UserThread;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.extern.slf4j.Slf4j;
import static bisq.common.file.FileUtil.deleteFileIfExists;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@Slf4j
public class ManualShutdown {
public static final String SHUTDOWN_FILENAME = "/tmp/bottest-shutdown";
private static final AtomicBoolean SHUTDOWN_CALLED = new AtomicBoolean(false);
/**
* Looks for a /tmp/bottest-shutdown file and throws a BotShutdownException if found.
*
* Running '$ touch /tmp/bottest-shutdown' could be used to trigger a scaffold teardown.
*
* This is much easier than manually shutdown down bisq apps & bitcoind.
*/
public static void startShutdownTimer() {
deleteStaleShutdownFile();
UserThread.runPeriodically(() -> {
File shutdownFile = new File(SHUTDOWN_FILENAME);
if (shutdownFile.exists()) {
log.warn("Caught manual shutdown signal: /tmp/bottest-shutdown file exists.");
try {
deleteFileIfExists(shutdownFile);
} catch (IOException ex) {
log.error("", ex);
throw new IllegalStateException(ex);
}
SHUTDOWN_CALLED.set(true);
}
}, 2000, MILLISECONDS);
}
public static boolean isShutdownCalled() {
return SHUTDOWN_CALLED.get();
}
public static void checkIfShutdownCalled(String warning) throws ManualBotShutdownException {
if (isShutdownCalled())
throw new ManualBotShutdownException(warning);
}
private static void deleteStaleShutdownFile() {
try {
deleteFileIfExists(new File(SHUTDOWN_FILENAME));
} catch (IOException ex) {
log.error("", ex);
throw new IllegalStateException(ex);
}
}
}

View file

@ -63,7 +63,7 @@ configure(subprojects) {
loggingVersion = '1.2'
lombokVersion = '1.18.2'
mockitoVersion = '3.0.0'
netlayerVersion = 'cc80787'
netlayerVersion = '32779ac' // Commit ID from https://github.com/bisq-network/netlayer/commits/externaltor
protobufVersion = '3.10.0'
protocVersion = protobufVersion
pushyVersion = '0.13.2'
@ -364,6 +364,17 @@ configure(project(':cli')) {
implementation "ch.qos.logback:logback-classic:$logbackVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion")
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testRuntime "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
}
test {
useJUnitPlatform()
}
}
@ -372,7 +383,7 @@ configure(project(':desktop')) {
apply plugin: 'witness'
apply from: '../gradle/witness/gradle-witness.gradle'
version = '1.5.4-SNAPSHOT'
version = '1.6.0'
mainClassName = 'bisq.desktop.app.BisqAppMain'

View file

@ -17,43 +17,7 @@
package bisq.cli;
import bisq.proto.grpc.CancelOfferRequest;
import bisq.proto.grpc.ConfirmPaymentReceivedRequest;
import bisq.proto.grpc.ConfirmPaymentStartedRequest;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetMethodHelpRequest;
import bisq.proto.grpc.GetMyOfferRequest;
import bisq.proto.grpc.GetMyOffersRequest;
import bisq.proto.grpc.GetOfferRequest;
import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
import bisq.proto.grpc.GetVersionRequest;
import bisq.proto.grpc.KeepFundsRequest;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TxInfo;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WithdrawFundsRequest;
import protobuf.PaymentAccount;
import io.grpc.StatusRuntimeException;
@ -75,16 +39,13 @@ import java.util.List;
import lombok.extern.slf4j.Slf4j;
import static bisq.cli.CurrencyFormat.formatMarketPrice;
import static bisq.cli.CurrencyFormat.formatTxFeeRateInfo;
import static bisq.cli.CurrencyFormat.toSatoshis;
import static bisq.cli.CurrencyFormat.toSecurityDepositAsPct;
import static bisq.cli.Method.*;
import static bisq.cli.TableFormat.*;
import static bisq.cli.opts.OptLabel.OPT_HELP;
import static bisq.cli.opts.OptLabel.OPT_HOST;
import static bisq.cli.opts.OptLabel.OPT_PASSWORD;
import static bisq.cli.opts.OptLabel.OPT_PORT;
import static bisq.proto.grpc.HelpGrpc.HelpBlockingStub;
import static bisq.cli.opts.OptLabel.*;
import static java.lang.String.format;
import static java.lang.System.err;
import static java.lang.System.exit;
@ -98,6 +59,7 @@ import bisq.cli.opts.CancelOfferOptionParser;
import bisq.cli.opts.CreateOfferOptionParser;
import bisq.cli.opts.CreatePaymentAcctOptionParser;
import bisq.cli.opts.GetAddressBalanceOptionParser;
import bisq.cli.opts.GetBTCMarketPriceOptionParser;
import bisq.cli.opts.GetBalanceOptionParser;
import bisq.cli.opts.GetOfferOptionParser;
import bisq.cli.opts.GetOffersOptionParser;
@ -118,7 +80,6 @@ import bisq.cli.opts.WithdrawFundsOptionParser;
/**
* A command-line client for the Bisq gRPC API.
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
@Slf4j
public class CliMain {
@ -184,48 +145,36 @@ public class CliMain {
throw new IllegalArgumentException(format("'%s' is not a supported method", methodName));
}
GrpcStubs grpcStubs = new GrpcStubs(host, port, password);
var disputeAgentsService = grpcStubs.disputeAgentsService;
var helpService = grpcStubs.helpService;
var offersService = grpcStubs.offersService;
var paymentAccountsService = grpcStubs.paymentAccountsService;
var tradesService = grpcStubs.tradesService;
var versionService = grpcStubs.versionService;
var walletsService = grpcStubs.walletsService;
GrpcClient client = new GrpcClient(host, port, password);
try {
switch (method) {
case getversion: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = GetVersionRequest.newBuilder().build();
var version = versionService.getVersion(request).getVersion();
var version = client.getVersion();
out.println(version);
return;
}
case getbalance: {
var opts = new GetBalanceOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var currencyCode = opts.getCurrencyCode();
var request = GetBalancesRequest.newBuilder()
.setCurrencyCode(currencyCode)
.build();
var reply = walletsService.getBalances(request);
var balances = client.getBalances(currencyCode);
switch (currencyCode.toUpperCase()) {
case "BSQ":
out.println(formatBsqBalanceInfoTbl(reply.getBalances().getBsq()));
out.println(formatBsqBalanceInfoTbl(balances.getBsq()));
break;
case "BTC":
out.println(formatBtcBalanceInfoTbl(reply.getBalances().getBtc()));
out.println(formatBtcBalanceInfoTbl(balances.getBtc()));
break;
case "":
default:
out.println(formatBalancesTbls(reply.getBalances()));
out.println(formatBalancesTbls(balances));
break;
}
return;
@ -233,57 +182,58 @@ public class CliMain {
case getaddressbalance: {
var opts = new GetAddressBalanceOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var address = opts.getAddress();
var request = GetAddressBalanceRequest.newBuilder()
.setAddress(address).build();
var reply = walletsService.getAddressBalance(request);
out.println(formatAddressBalanceTbl(singletonList(reply.getAddressBalanceInfo())));
var addressBalance = client.getAddressBalance(address);
out.println(formatAddressBalanceTbl(singletonList(addressBalance)));
return;
}
case getbtcprice: {
var opts = new GetBTCMarketPriceOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var currencyCode = opts.getCurrencyCode();
var price = client.getBtcPrice(currencyCode);
out.println(formatMarketPrice(price));
return;
}
case getfundingaddresses: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = GetFundingAddressesRequest.newBuilder().build();
var reply = walletsService.getFundingAddresses(request);
out.println(formatAddressBalanceTbl(reply.getAddressBalanceInfoList()));
var fundingAddresses = client.getFundingAddresses();
out.println(formatAddressBalanceTbl(fundingAddresses));
return;
}
case getunusedbsqaddress: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = GetUnusedBsqAddressRequest.newBuilder().build();
var reply = walletsService.getUnusedBsqAddress(request);
out.println(reply.getAddress());
var address = client.getUnusedBsqAddress();
out.println(address);
return;
}
case sendbsq: {
var opts = new SendBsqOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var address = opts.getAddress();
var amount = opts.getAmount();
verifyStringIsValidDecimal(amount);
verifyStringIsValidDecimal(OPT_AMOUNT, amount);
var txFeeRate = opts.getFeeRate();
if (txFeeRate.isEmpty())
verifyStringIsValidLong(txFeeRate);
if (!txFeeRate.isEmpty())
verifyStringIsValidLong(OPT_TX_FEE_RATE, txFeeRate);
var request = SendBsqRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.build();
var reply = walletsService.sendBsq(request);
TxInfo txInfo = reply.getTxInfo();
var txInfo = client.sendBsq(address, amount, txFeeRate);
out.printf("%s bsq sent to %s in tx %s%n",
amount,
address,
@ -293,26 +243,20 @@ public class CliMain {
case sendbtc: {
var opts = new SendBtcOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var address = opts.getAddress();
var amount = opts.getAmount();
verifyStringIsValidDecimal(amount);
verifyStringIsValidDecimal(OPT_AMOUNT, amount);
var txFeeRate = opts.getFeeRate();
if (txFeeRate.isEmpty())
verifyStringIsValidLong(txFeeRate);
if (!txFeeRate.isEmpty())
verifyStringIsValidLong(OPT_TX_FEE_RATE, txFeeRate);
var memo = opts.getMemo();
var request = SendBtcRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.setMemo(memo)
.build();
var reply = walletsService.sendBtc(request);
TxInfo txInfo = reply.getTxInfo();
var txInfo = client.sendBtc(address, amount, txFeeRate, memo);
out.printf("%s btc sent to %s in tx %s%n",
amount,
address,
@ -321,56 +265,47 @@ public class CliMain {
}
case gettxfeerate: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = GetTxFeeRateRequest.newBuilder().build();
var reply = walletsService.getTxFeeRate(request);
out.println(formatTxFeeRateInfo(reply.getTxFeeRateInfo()));
var txFeeRate = client.getTxFeeRate();
out.println(formatTxFeeRateInfo(txFeeRate));
return;
}
case settxfeerate: {
var opts = new SetTxFeeRateOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var txFeeRate = toLong(opts.getFeeRate());
var request = SetTxFeeRatePreferenceRequest.newBuilder()
.setTxFeeRatePreference(txFeeRate)
.build();
var reply = walletsService.setTxFeeRatePreference(request);
out.println(formatTxFeeRateInfo(reply.getTxFeeRateInfo()));
var txFeeRate = client.setTxFeeRate(toLong(opts.getFeeRate()));
out.println(formatTxFeeRateInfo(txFeeRate));
return;
}
case unsettxfeerate: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = UnsetTxFeeRatePreferenceRequest.newBuilder().build();
var reply = walletsService.unsetTxFeeRatePreference(request);
out.println(formatTxFeeRateInfo(reply.getTxFeeRateInfo()));
var txFeeRate = client.unsetTxFeeRate();
out.println(formatTxFeeRateInfo(txFeeRate));
return;
}
case gettransaction: {
var opts = new GetTransactionOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var txId = opts.getTxId();
var request = GetTransactionRequest.newBuilder()
.setTxId(txId)
.build();
var reply = walletsService.getTransaction(request);
out.println(TransactionFormat.format(reply.getTxInfo()));
var tx = client.getTransaction(txId);
out.println(TransactionFormat.format(tx));
return;
}
case createoffer: {
var opts = new CreateOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var paymentAcctId = opts.getPaymentAccountId();
@ -383,232 +318,178 @@ public class CliMain {
var marketPriceMargin = opts.getMktPriceMarginAsBigDecimal();
var securityDeposit = toSecurityDepositAsPct(opts.getSecurityDeposit());
var makerFeeCurrencyCode = opts.getMakerFeeCurrencyCode();
var request = CreateOfferRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.setAmount(amount)
.setMinAmount(minAmount)
.setUseMarketBasedPrice(useMarketBasedPrice)
.setPrice(fixedPrice)
.setMarketPriceMargin(marketPriceMargin.doubleValue())
.setBuyerSecurityDeposit(securityDeposit)
.setPaymentAccountId(paymentAcctId)
.setMakerFeeCurrencyCode(makerFeeCurrencyCode)
.build();
var reply = offersService.createOffer(request);
out.println(formatOfferTable(singletonList(reply.getOffer()), currencyCode));
var offer = client.createOffer(direction,
currencyCode,
amount,
minAmount,
useMarketBasedPrice,
fixedPrice,
marketPriceMargin.doubleValue(),
securityDeposit,
paymentAcctId,
makerFeeCurrencyCode);
out.println(formatOfferTable(singletonList(offer), currencyCode));
return;
}
case canceloffer: {
var opts = new CancelOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var request = CancelOfferRequest.newBuilder()
.setId(offerId)
.build();
offersService.cancelOffer(request);
client.cancelOffer(offerId);
out.println("offer canceled and removed from offer book");
return;
}
case getoffer: {
var opts = new GetOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var request = GetOfferRequest.newBuilder()
.setId(offerId)
.build();
var reply = offersService.getOffer(request);
out.println(formatOfferTable(singletonList(reply.getOffer()),
reply.getOffer().getCounterCurrencyCode()));
var offer = client.getOffer(offerId);
out.println(formatOfferTable(singletonList(offer), offer.getCounterCurrencyCode()));
return;
}
case getmyoffer: {
var opts = new GetOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var request = GetMyOfferRequest.newBuilder()
.setId(offerId)
.build();
var reply = offersService.getMyOffer(request);
out.println(formatOfferTable(singletonList(reply.getOffer()),
reply.getOffer().getCounterCurrencyCode()));
var offer = client.getMyOffer(offerId);
out.println(formatOfferTable(singletonList(offer), offer.getCounterCurrencyCode()));
return;
}
case getoffers: {
var opts = new GetOffersOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var direction = opts.getDirection();
var currencyCode = opts.getCurrencyCode();
var request = GetOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.build();
var reply = offersService.getOffers(request);
List<OfferInfo> offers = reply.getOffersList();
List<OfferInfo> offers = client.getOffers(direction, currencyCode);
if (offers.isEmpty())
out.printf("no %s %s offers found%n", direction, currencyCode);
else
out.println(formatOfferTable(reply.getOffersList(), currencyCode));
out.println(formatOfferTable(offers, currencyCode));
return;
}
case getmyoffers: {
var opts = new GetOffersOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var direction = opts.getDirection();
var currencyCode = opts.getCurrencyCode();
var request = GetMyOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.build();
var reply = offersService.getMyOffers(request);
List<OfferInfo> offers = reply.getOffersList();
List<OfferInfo> offers = client.getMyOffers(direction, currencyCode);
if (offers.isEmpty())
out.printf("no %s %s offers found%n", direction, currencyCode);
else
out.println(formatOfferTable(reply.getOffersList(), currencyCode));
out.println(formatOfferTable(offers, currencyCode));
return;
}
case takeoffer: {
var opts = new TakeOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var paymentAccountId = opts.getPaymentAccountId();
var takerFeeCurrencyCode = opts.getTakerFeeCurrencyCode();
var request = TakeOfferRequest.newBuilder()
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId)
.setTakerFeeCurrencyCode(takerFeeCurrencyCode)
.build();
var reply = tradesService.takeOffer(request);
out.printf("trade %s successfully taken%n", reply.getTrade().getTradeId());
var trade = client.takeOffer(offerId, paymentAccountId, takerFeeCurrencyCode);
out.printf("trade %s successfully taken%n", trade.getTradeId());
return;
}
case gettrade: {
// TODO make short-id a valid argument?
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
var showContract = opts.getShowContract();
var request = GetTradeRequest.newBuilder()
.setTradeId(tradeId)
.build();
var reply = tradesService.getTrade(request);
var trade = client.getTrade(tradeId);
if (showContract)
out.println(reply.getTrade().getContractAsJson());
out.println(trade.getContractAsJson());
else
out.println(TradeFormat.format(reply.getTrade()));
out.println(TradeFormat.format(trade));
return;
}
case confirmpaymentstarted: {
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
var request = ConfirmPaymentStartedRequest.newBuilder()
.setTradeId(tradeId)
.build();
tradesService.confirmPaymentStarted(request);
client.confirmPaymentStarted(tradeId);
out.printf("trade %s payment started message sent%n", tradeId);
return;
}
case confirmpaymentreceived: {
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
var request = ConfirmPaymentReceivedRequest.newBuilder()
.setTradeId(tradeId)
.build();
tradesService.confirmPaymentReceived(request);
client.confirmPaymentReceived(tradeId);
out.printf("trade %s payment received message sent%n", tradeId);
return;
}
case keepfunds: {
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
var request = KeepFundsRequest.newBuilder()
.setTradeId(tradeId)
.build();
tradesService.keepFunds(request);
client.keepFunds(tradeId);
out.printf("funds from trade %s saved in bisq wallet%n", tradeId);
return;
}
case withdrawfunds: {
var opts = new WithdrawFundsOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
var address = opts.getAddress();
// Multi-word memos must be double quoted.
var memo = opts.getMemo();
var request = WithdrawFundsRequest.newBuilder()
.setTradeId(tradeId)
.setAddress(address)
.setMemo(memo)
.build();
tradesService.withdrawFunds(request);
client.withdrawFunds(tradeId, address, memo);
out.printf("trade %s funds sent to btc address %s%n", tradeId, address);
return;
}
case getpaymentmethods: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = GetPaymentMethodsRequest.newBuilder().build();
var reply = paymentAccountsService.getPaymentMethods(request);
reply.getPaymentMethodsList().forEach(p -> out.println(p.getId()));
var paymentMethods = client.getPaymentMethods();
paymentMethods.forEach(p -> out.println(p.getId()));
return;
}
case getpaymentacctform: {
var opts = new GetPaymentAcctFormOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var paymentMethodId = opts.getPaymentMethodId();
var request = GetPaymentAccountFormRequest.newBuilder()
.setPaymentMethodId(paymentMethodId)
.build();
String jsonString = paymentAccountsService.getPaymentAccountForm(request)
.getPaymentAccountFormJson();
String jsonString = client.getPaymentAcctFormAsJson(paymentMethodId);
File jsonFile = saveFileToDisk(paymentMethodId.toLowerCase(),
".json",
jsonString);
@ -620,7 +501,7 @@ public class CliMain {
case createpaymentacct: {
var opts = new CreatePaymentAcctOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var paymentAccountForm = opts.getPaymentAcctForm();
@ -631,24 +512,17 @@ public class CliMain {
throw new IllegalStateException(
format("could not read %s", paymentAccountForm.toString()));
}
var request = CreatePaymentAccountRequest.newBuilder()
.setPaymentAccountForm(jsonString)
.build();
var reply = paymentAccountsService.createPaymentAccount(request);
var paymentAccount = client.createPaymentAccount(jsonString);
out.println("payment account saved");
out.println(formatPaymentAcctTbl(singletonList(reply.getPaymentAccount())));
out.println(formatPaymentAcctTbl(singletonList(paymentAccount)));
return;
}
case getpaymentaccts: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = GetPaymentAccountsRequest.newBuilder().build();
var reply = paymentAccountsService.getPaymentAccounts(request);
List<PaymentAccount> paymentAccounts = reply.getPaymentAccountsList();
var paymentAccounts = client.getPaymentAccounts();
if (paymentAccounts.size() > 0)
out.println(formatPaymentAcctTbl(paymentAccounts));
else
@ -658,71 +532,69 @@ public class CliMain {
}
case lockwallet: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = LockWalletRequest.newBuilder().build();
walletsService.lockWallet(request);
client.lockWallet();
out.println("wallet locked");
return;
}
case unlockwallet: {
var opts = new UnlockWalletOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var walletPassword = opts.getPassword();
var timeout = opts.getUnlockTimeout();
var request = UnlockWalletRequest.newBuilder()
.setPassword(walletPassword)
.setTimeout(timeout).build();
walletsService.unlockWallet(request);
client.unlockWallet(walletPassword, timeout);
out.println("wallet unlocked");
return;
}
case removewalletpassword: {
var opts = new RemoveWalletPasswordOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var walletPassword = opts.getPassword();
var request = RemoveWalletPasswordRequest.newBuilder()
.setPassword(walletPassword).build();
walletsService.removeWalletPassword(request);
client.removeWalletPassword(walletPassword);
out.println("wallet decrypted");
return;
}
case setwalletpassword: {
var opts = new SetWalletPasswordOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var walletPassword = opts.getPassword();
var newWalletPassword = opts.getNewPassword();
var requestBuilder = SetWalletPasswordRequest.newBuilder()
.setPassword(walletPassword)
.setNewPassword(newWalletPassword);
walletsService.setWalletPassword(requestBuilder.build());
client.setWalletPassword(walletPassword, newWalletPassword);
out.println("wallet encrypted" + (!newWalletPassword.isEmpty() ? " with new password" : ""));
return;
}
case registerdisputeagent: {
var opts = new RegisterDisputeAgentOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var disputeAgentType = opts.getDisputeAgentType();
var registrationKey = opts.getRegistrationKey();
var requestBuilder = RegisterDisputeAgentRequest.newBuilder()
.setDisputeAgentType(disputeAgentType).setRegistrationKey(registrationKey);
disputeAgentsService.registerDisputeAgent(requestBuilder.build());
client.registerDisputeAgent(disputeAgentType, registrationKey);
out.println(disputeAgentType + " registered");
return;
}
case stop: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
client.stopServer();
out.println("server shutdown signal received");
return;
}
default: {
throw new RuntimeException(format("unhandled method '%s'", method));
}
@ -730,7 +602,10 @@ public class CliMain {
} catch (StatusRuntimeException ex) {
// Remove the leading gRPC status code (e.g. "UNKNOWN: ") from the message
String message = ex.getMessage().replaceFirst("^[A-Z_]+: ", "");
throw new RuntimeException(message, ex);
if (message.equals("io exception"))
throw new RuntimeException(message + ", server may not be running", ex);
else
throw new RuntimeException(message, ex);
}
}
@ -741,19 +616,27 @@ public class CliMain {
return Method.valueOf(methodName.toLowerCase());
}
private static void verifyStringIsValidDecimal(String param) {
@SuppressWarnings("SameParameterValue")
private static void verifyStringIsValidDecimal(String optionLabel, String optionValue) {
try {
Double.parseDouble(param);
Double.parseDouble(optionValue);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(format("'%s' is not a number", param));
throw new IllegalArgumentException(format("--%s=%s, '%s' is not a number",
optionLabel,
optionValue,
optionValue));
}
}
private static void verifyStringIsValidLong(String param) {
@SuppressWarnings("SameParameterValue")
private static void verifyStringIsValidLong(String optionLabel, String optionValue) {
try {
Long.parseLong(param);
Long.parseLong(optionValue);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(format("'%s' is not a number", param));
throw new IllegalArgumentException(format("--%s=%s, '%s' is not a number",
optionLabel,
optionValue,
optionValue));
}
}
@ -802,15 +685,18 @@ public class CliMain {
stream.println();
stream.format(rowFormat, getaddressbalance.name(), "--address=<btc-address>", "Get server wallet address balance");
stream.println();
stream.format(rowFormat, getbtcprice.name(), "--currency-code=<currency-code>", "Get current market btc price");
stream.println();
stream.format(rowFormat, getfundingaddresses.name(), "", "Get BTC funding addresses");
stream.println();
stream.format(rowFormat, getunusedbsqaddress.name(), "", "Get unused BSQ address");
stream.println();
stream.format(rowFormat, sendbsq.name(), "--address=<btc-address> --amount=<btc-amount> \\", "Send BSQ");
stream.format(rowFormat, sendbsq.name(), "--address=<bsq-address> --amount=<bsq-amount> \\", "Send BSQ");
stream.format(rowFormat, "", "[--tx-fee-rate=<sats/byte>]", "");
stream.println();
stream.format(rowFormat, sendbtc.name(), "--address=<bsq-address> --amount=<bsq-amount> \\", "Send BTC");
stream.format(rowFormat, sendbtc.name(), "--address=<btc-address> --amount=<btc-amount> \\", "Send BTC");
stream.format(rowFormat, "", "[--tx-fee-rate=<sats/byte>]", "");
stream.format(rowFormat, "", "[--memo=<\"memo\">]", "");
stream.println();
stream.format(rowFormat, gettxfeerate.name(), "", "Get current tx fee rate in sats/byte");
stream.println();
@ -873,16 +759,12 @@ public class CliMain {
"Encrypt wallet with password, or set new password on encrypted wallet");
stream.format(rowFormat, "", "[--new-wallet-password=<new-password>]", "");
stream.println();
stream.format(rowFormat, stop.name(), "", "Shut down the server");
stream.println();
stream.println("Method Help Usage: bisq-cli [options] <method> --help");
stream.println();
} catch (IOException ex) {
ex.printStackTrace(stream);
}
}
private static String getMethodHelp(HelpBlockingStub helpService, Method method) {
var request = GetMethodHelpRequest.newBuilder().setMethodName(method.name()).build();
var reply = helpService.getMethodHelp(request);
return reply.getMethodHelp();
}
}

View file

@ -27,8 +27,8 @@ class ColumnHeaderConstants {
// Table column header format specs, right padded with two spaces. In some cases
// such as COL_HEADER_CREATION_DATE, COL_HEADER_VOLUME and COL_HEADER_UUID, the
// expected max data string length is accounted for. In others, the column header length
// are expected to be greater than any column value length.
// expected max data string length is accounted for. In others, column header
// lengths are expected to be greater than any column value length.
static final String COL_HEADER_ADDRESS = padEnd("%-3s Address", 52, ' ');
static final String COL_HEADER_AMOUNT = padEnd("BTC(min - max)", 24, ' ');
static final String COL_HEADER_AVAILABLE_BALANCE = "Available Balance";
@ -42,6 +42,7 @@ class ColumnHeaderConstants {
static final String COL_HEADER_UNLOCKING_BONDS_BALANCE = "Unlocking Bonds Balance";
static final String COL_HEADER_UNVERIFIED_BALANCE = "Unverified Balance";
static final String COL_HEADER_CONFIRMATIONS = "Confirmations";
static final String COL_HEADER_IS_USED_ADDRESS = "Is Used";
static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date (UTC)", 20, ' ');
static final String COL_HEADER_CURRENCY = "Currency";
static final String COL_HEADER_DIRECTION = "Buy/Sell";
@ -49,17 +50,17 @@ class ColumnHeaderConstants {
static final String COL_HEADER_PAYMENT_METHOD = "Payment Method";
static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC";
static final String COL_HEADER_TRADE_AMOUNT = padStart("Amount(%-3s)", 12, ' ');
static final String COL_HEADER_TRADE_BUYER_COST = padEnd("Buyer Cost(%-3s)", 15, ' ');
static final String COL_HEADER_TRADE_DEPOSIT_CONFIRMED = "Deposit Confirmed";
static final String COL_HEADER_TRADE_DEPOSIT_PUBLISHED = "Deposit Published";
static final String COL_HEADER_TRADE_FIAT_SENT = "Fiat Sent";
static final String COL_HEADER_TRADE_FIAT_RECEIVED = "Fiat Received";
static final String COL_HEADER_TRADE_PAYMENT_SENT = padEnd("%-3s Sent", 8, ' ');
static final String COL_HEADER_TRADE_PAYMENT_RECEIVED = padEnd("%-3s Received", 12, ' ');
static final String COL_HEADER_TRADE_PAYOUT_PUBLISHED = "Payout Published";
static final String COL_HEADER_TRADE_WITHDRAWN = "Withdrawn";
static final String COL_HEADER_TRADE_ROLE = "My Role";
static final String COL_HEADER_TRADE_SHORT_ID = "ID";
static final String COL_HEADER_TRADE_TX_FEE = "Tx Fee(%-3s)";
static final String COL_HEADER_TRADE_TAKER_FEE = "Taker Fee(%-3s)";
static final String COL_HEADER_TRADE_WITHDRAWAL_TX_ID = "Withdrawal TX ID";
static final String COL_HEADER_TX_ID = "Tx ID";
static final String COL_HEADER_TX_INPUT_SUM = "Tx Inputs (BTC)";

View file

@ -38,7 +38,7 @@ public class CurrencyFormat {
static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100000000);
static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000");
static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,##0.00");
static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,###,##0");
static final BigDecimal BSQ_SATOSHI_DIVISOR = new BigDecimal(100);
static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00");
@ -65,32 +65,37 @@ public class CurrencyFormat {
formatFeeSatoshis(txFeeRateInfo.getFeeServiceRate()));
}
static String formatAmountRange(long minAmount, long amount) {
public static String formatAmountRange(long minAmount, long amount) {
return minAmount != amount
? formatSatoshis(minAmount) + " - " + formatSatoshis(amount)
: formatSatoshis(amount);
}
static String formatVolumeRange(long minVolume, long volume) {
public static String formatVolumeRange(long minVolume, long volume) {
return minVolume != volume
? formatOfferVolume(minVolume) + " - " + formatOfferVolume(volume)
: formatOfferVolume(volume);
}
static String formatOfferPrice(long price) {
public static String formatMarketPrice(double price) {
NUMBER_FORMAT.setMinimumFractionDigits(4);
return NUMBER_FORMAT.format(price);
}
public static String formatOfferPrice(long price) {
NUMBER_FORMAT.setMaximumFractionDigits(4);
NUMBER_FORMAT.setMinimumFractionDigits(4);
NUMBER_FORMAT.setRoundingMode(RoundingMode.UNNECESSARY);
return NUMBER_FORMAT.format((double) price / 10000);
}
static String formatOfferVolume(long volume) {
public static String formatOfferVolume(long volume) {
NUMBER_FORMAT.setMaximumFractionDigits(0);
NUMBER_FORMAT.setRoundingMode(RoundingMode.UNNECESSARY);
return NUMBER_FORMAT.format((double) volume / 10000);
}
static long toSatoshis(String btc) {
public static long toSatoshis(String btc) {
if (btc.startsWith("-"))
throw new IllegalArgumentException(format("'%s' is not a positive number", btc));
@ -101,7 +106,7 @@ public class CurrencyFormat {
}
}
static double toSecurityDepositAsPct(String securityDepositInput) {
public static double toSecurityDepositAsPct(String securityDepositInput) {
try {
return new BigDecimal(securityDepositInput)
.multiply(SECURITY_DEPOSIT_MULTIPLICAND).doubleValue();
@ -110,8 +115,7 @@ public class CurrencyFormat {
}
}
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
private static String formatFeeSatoshis(long sats) {
return BTC_TX_FEE_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR));
public static String formatFeeSatoshis(long sats) {
return BTC_TX_FEE_FORMAT.format(BigDecimal.valueOf(sats));
}
}

View file

@ -0,0 +1,453 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.BsqBalanceInfo;
import bisq.proto.grpc.BtcBalanceInfo;
import bisq.proto.grpc.CancelOfferRequest;
import bisq.proto.grpc.ConfirmPaymentReceivedRequest;
import bisq.proto.grpc.ConfirmPaymentStartedRequest;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetMethodHelpRequest;
import bisq.proto.grpc.GetMyOfferRequest;
import bisq.proto.grpc.GetMyOffersRequest;
import bisq.proto.grpc.GetOfferRequest;
import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
import bisq.proto.grpc.GetVersionRequest;
import bisq.proto.grpc.KeepFundsRequest;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.StopRequest;
import bisq.proto.grpc.TakeOfferReply;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TradeInfo;
import bisq.proto.grpc.TxFeeRateInfo;
import bisq.proto.grpc.TxInfo;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WithdrawFundsRequest;
import protobuf.PaymentAccount;
import protobuf.PaymentMethod;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import static java.util.Comparator.comparing;
import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OfferPayload.Direction.SELL;
@SuppressWarnings("ResultOfMethodCallIgnored")
@Slf4j
public final class GrpcClient {
private final GrpcStubs grpcStubs;
public GrpcClient(String apiHost, int apiPort, String apiPassword) {
this.grpcStubs = new GrpcStubs(apiHost, apiPort, apiPassword);
}
public String getVersion() {
var request = GetVersionRequest.newBuilder().build();
return grpcStubs.versionService.getVersion(request).getVersion();
}
public BalancesInfo getBalances() {
return getBalances("");
}
public BsqBalanceInfo getBsqBalances() {
return getBalances("BSQ").getBsq();
}
public BtcBalanceInfo getBtcBalances() {
return getBalances("BTC").getBtc();
}
public BalancesInfo getBalances(String currencyCode) {
var request = GetBalancesRequest.newBuilder()
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.walletsService.getBalances(request).getBalances();
}
public AddressBalanceInfo getAddressBalance(String address) {
var request = GetAddressBalanceRequest.newBuilder()
.setAddress(address).build();
return grpcStubs.walletsService.getAddressBalance(request).getAddressBalanceInfo();
}
public double getBtcPrice(String currencyCode) {
var request = MarketPriceRequest.newBuilder()
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.priceService.getMarketPrice(request).getPrice();
}
public List<AddressBalanceInfo> getFundingAddresses() {
var request = GetFundingAddressesRequest.newBuilder().build();
return grpcStubs.walletsService.getFundingAddresses(request).getAddressBalanceInfoList();
}
public String getUnusedBsqAddress() {
var request = GetUnusedBsqAddressRequest.newBuilder().build();
return grpcStubs.walletsService.getUnusedBsqAddress(request).getAddress();
}
public String getUnusedBtcAddress() {
var request = GetFundingAddressesRequest.newBuilder().build();
var addressBalances = grpcStubs.walletsService.getFundingAddresses(request)
.getAddressBalanceInfoList();
//noinspection OptionalGetWithoutIsPresent
return addressBalances.stream()
.filter(AddressBalanceInfo::getIsAddressUnused)
.findFirst()
.get()
.getAddress();
}
public TxInfo sendBsq(String address, String amount, String txFeeRate) {
var request = SendBsqRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.build();
return grpcStubs.walletsService.sendBsq(request).getTxInfo();
}
public TxInfo sendBtc(String address, String amount, String txFeeRate, String memo) {
var request = SendBtcRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.setMemo(memo)
.build();
return grpcStubs.walletsService.sendBtc(request).getTxInfo();
}
public TxFeeRateInfo getTxFeeRate() {
var request = GetTxFeeRateRequest.newBuilder().build();
return grpcStubs.walletsService.getTxFeeRate(request).getTxFeeRateInfo();
}
public TxFeeRateInfo setTxFeeRate(long txFeeRate) {
var request = SetTxFeeRatePreferenceRequest.newBuilder()
.setTxFeeRatePreference(txFeeRate)
.build();
return grpcStubs.walletsService.setTxFeeRatePreference(request).getTxFeeRateInfo();
}
public TxFeeRateInfo unsetTxFeeRate() {
var request = UnsetTxFeeRatePreferenceRequest.newBuilder().build();
return grpcStubs.walletsService.unsetTxFeeRatePreference(request).getTxFeeRateInfo();
}
public TxInfo getTransaction(String txId) {
var request = GetTransactionRequest.newBuilder()
.setTxId(txId)
.build();
return grpcStubs.walletsService.getTransaction(request).getTxInfo();
}
public OfferInfo createFixedPricedOffer(String direction,
String currencyCode,
long amount,
long minAmount,
String fixedPrice,
double securityDeposit,
String paymentAcctId,
String makerFeeCurrencyCode) {
return createOffer(direction,
currencyCode,
amount,
minAmount,
false,
fixedPrice,
0.00,
securityDeposit,
paymentAcctId,
makerFeeCurrencyCode);
}
public OfferInfo createMarketBasedPricedOffer(String direction,
String currencyCode,
long amount,
long minAmount,
double marketPriceMargin,
double securityDeposit,
String paymentAcctId,
String makerFeeCurrencyCode) {
return createOffer(direction,
currencyCode,
amount,
minAmount,
true,
"0",
marketPriceMargin,
securityDeposit,
paymentAcctId,
makerFeeCurrencyCode);
}
// TODO make private, move to bottom of class
public OfferInfo createOffer(String direction,
String currencyCode,
long amount,
long minAmount,
boolean useMarketBasedPrice,
String fixedPrice,
double marketPriceMargin,
double securityDeposit,
String paymentAcctId,
String makerFeeCurrencyCode) {
var request = CreateOfferRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.setAmount(amount)
.setMinAmount(minAmount)
.setUseMarketBasedPrice(useMarketBasedPrice)
.setPrice(fixedPrice)
.setMarketPriceMargin(marketPriceMargin)
.setBuyerSecurityDeposit(securityDeposit)
.setPaymentAccountId(paymentAcctId)
.setMakerFeeCurrencyCode(makerFeeCurrencyCode)
.build();
return grpcStubs.offersService.createOffer(request).getOffer();
}
public void cancelOffer(String offerId) {
var request = CancelOfferRequest.newBuilder()
.setId(offerId)
.build();
grpcStubs.offersService.cancelOffer(request);
}
public OfferInfo getOffer(String offerId) {
var request = GetOfferRequest.newBuilder()
.setId(offerId)
.build();
return grpcStubs.offersService.getOffer(request).getOffer();
}
public OfferInfo getMyOffer(String offerId) {
var request = GetMyOfferRequest.newBuilder()
.setId(offerId)
.build();
return grpcStubs.offersService.getMyOffer(request).getOffer();
}
public List<OfferInfo> getOffers(String direction, String currencyCode) {
var request = GetOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.offersService.getOffers(request).getOffersList();
}
public List<OfferInfo> getOffersSortedByDate(String currencyCode) {
ArrayList<OfferInfo> offers = new ArrayList<>();
offers.addAll(getOffers(BUY.name(), currencyCode));
offers.addAll(getOffers(SELL.name(), currencyCode));
return sortOffersByDate(offers);
}
public List<OfferInfo> getOffersSortedByDate(String direction, String currencyCode) {
var offers = getOffers(direction, currencyCode);
return offers.isEmpty() ? offers : sortOffersByDate(offers);
}
public List<OfferInfo> getMyOffers(String direction, String currencyCode) {
var request = GetMyOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.offersService.getMyOffers(request).getOffersList();
}
public List<OfferInfo> getMyOffersSortedByDate(String direction, String currencyCode) {
var offers = getMyOffers(direction, currencyCode);
return offers.isEmpty() ? offers : sortOffersByDate(offers);
}
public List<OfferInfo> getMyOffersSortedByDate(String currencyCode) {
ArrayList<OfferInfo> offers = new ArrayList<>();
offers.addAll(getMyOffers(BUY.name(), currencyCode));
offers.addAll(getMyOffers(SELL.name(), currencyCode));
return sortOffersByDate(offers);
}
public OfferInfo getMostRecentOffer(String direction, String currencyCode) {
List<OfferInfo> offers = getOffersSortedByDate(direction, currencyCode);
return offers.isEmpty() ? null : offers.get(offers.size() - 1);
}
public List<OfferInfo> sortOffersByDate(List<OfferInfo> offerInfoList) {
return offerInfoList.stream()
.sorted(comparing(OfferInfo::getDate))
.collect(Collectors.toList());
}
public TakeOfferReply getTakeOfferReply(String offerId, String paymentAccountId, String takerFeeCurrencyCode) {
var request = TakeOfferRequest.newBuilder()
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId)
.setTakerFeeCurrencyCode(takerFeeCurrencyCode)
.build();
return grpcStubs.tradesService.takeOffer(request);
}
public TradeInfo takeOffer(String offerId, String paymentAccountId, String takerFeeCurrencyCode) {
var reply = getTakeOfferReply(offerId, paymentAccountId, takerFeeCurrencyCode);
if (reply.hasTrade())
return reply.getTrade();
else
throw new IllegalStateException(reply.getAvailabilityResultDescription());
}
public TradeInfo getTrade(String tradeId) {
var request = GetTradeRequest.newBuilder()
.setTradeId(tradeId)
.build();
return grpcStubs.tradesService.getTrade(request).getTrade();
}
public void confirmPaymentStarted(String tradeId) {
var request = ConfirmPaymentStartedRequest.newBuilder()
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.confirmPaymentStarted(request);
}
public void confirmPaymentReceived(String tradeId) {
var request = ConfirmPaymentReceivedRequest.newBuilder()
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.confirmPaymentReceived(request);
}
public void keepFunds(String tradeId) {
var request = KeepFundsRequest.newBuilder()
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.keepFunds(request);
}
public void withdrawFunds(String tradeId, String address, String memo) {
var request = WithdrawFundsRequest.newBuilder()
.setTradeId(tradeId)
.setAddress(address)
.setMemo(memo)
.build();
grpcStubs.tradesService.withdrawFunds(request);
}
public List<PaymentMethod> getPaymentMethods() {
var request = GetPaymentMethodsRequest.newBuilder().build();
return grpcStubs.paymentAccountsService.getPaymentMethods(request).getPaymentMethodsList();
}
public String getPaymentAcctFormAsJson(String paymentMethodId) {
var request = GetPaymentAccountFormRequest.newBuilder()
.setPaymentMethodId(paymentMethodId)
.build();
return grpcStubs.paymentAccountsService.getPaymentAccountForm(request).getPaymentAccountFormJson();
}
public PaymentAccount createPaymentAccount(String json) {
var request = CreatePaymentAccountRequest.newBuilder()
.setPaymentAccountForm(json)
.build();
return grpcStubs.paymentAccountsService.createPaymentAccount(request).getPaymentAccount();
}
public List<PaymentAccount> getPaymentAccounts() {
var request = GetPaymentAccountsRequest.newBuilder().build();
return grpcStubs.paymentAccountsService.getPaymentAccounts(request).getPaymentAccountsList();
}
public void lockWallet() {
var request = LockWalletRequest.newBuilder().build();
grpcStubs.walletsService.lockWallet(request);
}
public void unlockWallet(String walletPassword, long timeout) {
var request = UnlockWalletRequest.newBuilder()
.setPassword(walletPassword)
.setTimeout(timeout).build();
grpcStubs.walletsService.unlockWallet(request);
}
public void removeWalletPassword(String walletPassword) {
var request = RemoveWalletPasswordRequest.newBuilder()
.setPassword(walletPassword).build();
grpcStubs.walletsService.removeWalletPassword(request);
}
public void setWalletPassword(String walletPassword) {
var request = SetWalletPasswordRequest.newBuilder()
.setPassword(walletPassword).build();
grpcStubs.walletsService.setWalletPassword(request);
}
public void setWalletPassword(String oldWalletPassword, String newWalletPassword) {
var request = SetWalletPasswordRequest.newBuilder()
.setPassword(oldWalletPassword)
.setNewPassword(newWalletPassword).build();
grpcStubs.walletsService.setWalletPassword(request);
}
public void registerDisputeAgent(String disputeAgentType, String registrationKey) {
var request = RegisterDisputeAgentRequest.newBuilder()
.setDisputeAgentType(disputeAgentType).setRegistrationKey(registrationKey).build();
grpcStubs.disputeAgentsService.registerDisputeAgent(request);
}
public void stopServer() {
var request = StopRequest.newBuilder().build();
grpcStubs.shutdownService.stop(request);
}
public String getMethodHelp(Method method) {
var request = GetMethodHelpRequest.newBuilder().setMethodName(method.name()).build();
return grpcStubs.helpService.getMethodHelp(request).getMethodHelp();
}
}

View file

@ -23,6 +23,7 @@ import bisq.proto.grpc.HelpGrpc;
import bisq.proto.grpc.OffersGrpc;
import bisq.proto.grpc.PaymentAccountsGrpc;
import bisq.proto.grpc.PriceGrpc;
import bisq.proto.grpc.ShutdownServerGrpc;
import bisq.proto.grpc.TradesGrpc;
import bisq.proto.grpc.WalletsGrpc;
@ -31,7 +32,7 @@ import io.grpc.ManagedChannelBuilder;
import static java.util.concurrent.TimeUnit.SECONDS;
public class GrpcStubs {
public final class GrpcStubs {
public final DisputeAgentsGrpc.DisputeAgentsBlockingStub disputeAgentsService;
public final HelpGrpc.HelpBlockingStub helpService;
@ -39,6 +40,7 @@ public class GrpcStubs {
public final OffersGrpc.OffersBlockingStub offersService;
public final PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService;
public final PriceGrpc.PriceBlockingStub priceService;
public final ShutdownServerGrpc.ShutdownServerBlockingStub shutdownService;
public final TradesGrpc.TradesBlockingStub tradesService;
public final WalletsGrpc.WalletsBlockingStub walletsService;
@ -60,6 +62,7 @@ public class GrpcStubs {
this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.priceService = PriceGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.shutdownService = ShutdownServerGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.tradesService = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
}

View file

@ -21,36 +21,38 @@ package bisq.cli;
* Currently supported api methods.
*/
public enum Method {
createoffer,
canceloffer,
getoffer,
getmyoffer,
getoffers,
getmyoffers,
takeoffer,
gettrade,
confirmpaymentstarted,
confirmpaymentreceived,
keepfunds,
withdrawfunds,
getpaymentmethods,
getpaymentacctform,
confirmpaymentstarted,
createoffer,
createpaymentacct,
getpaymentaccts,
getversion,
getbalance,
getaddressbalance,
getbalance,
getbtcprice,
getfundingaddresses,
getmyoffer,
getmyoffers,
getoffer,
getoffers,
getpaymentacctform,
getpaymentaccts,
getpaymentmethods,
gettrade,
gettransaction,
gettxfeerate,
getunusedbsqaddress,
getversion,
keepfunds,
lockwallet,
registerdisputeagent,
removewalletpassword,
sendbsq,
sendbtc,
gettxfeerate,
settxfeerate,
unsettxfeerate,
gettransaction,
lockwallet,
unlockwallet,
removewalletpassword,
setwalletpassword,
registerdisputeagent
takeoffer,
unlockwallet,
unsettxfeerate,
withdrawfunds,
stop
}

View file

@ -51,18 +51,21 @@ public class TableFormat {
public static String formatAddressBalanceTbl(List<AddressBalanceInfo> addressBalanceInfo) {
String headerFormatString = COL_HEADER_ADDRESS + COL_HEADER_DELIMITER
+ COL_HEADER_AVAILABLE_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_CONFIRMATIONS + COL_HEADER_DELIMITER + "\n";
+ COL_HEADER_CONFIRMATIONS + COL_HEADER_DELIMITER
+ COL_HEADER_IS_USED_ADDRESS + COL_HEADER_DELIMITER + "\n";
String headerLine = format(headerFormatString, "BTC");
String colDataFormat = "%-" + COL_HEADER_ADDRESS.length() + "s" // lt justify
+ " %" + (COL_HEADER_AVAILABLE_BALANCE.length() - 1) + "s" // rt justify
+ " %" + COL_HEADER_CONFIRMATIONS.length() + "d"; // lt justify
+ " %" + COL_HEADER_CONFIRMATIONS.length() + "d" // lt justify
+ " %" + COL_HEADER_IS_USED_ADDRESS.length() + "s"; // lt justify
return headerLine
+ addressBalanceInfo.stream()
.map(info -> format(colDataFormat,
info.getAddress(),
formatSatoshis(info.getBalance()),
info.getNumConfirmations()))
info.getNumConfirmations(),
info.getIsAddressUnused() ? "NO" : "YES"))
.collect(Collectors.joining("\n"));
}
@ -111,7 +114,7 @@ public class TableFormat {
formatSatoshis(btcBalanceInfo.getLockedBalance()));
}
static String formatOfferTable(List<OfferInfo> offerInfo, String fiatCurrency) {
public static String formatOfferTable(List<OfferInfo> offerInfo, String fiatCurrency) {
// Some column values might be longer than header, so we need to calculate them.
int paymentMethodColWidth = getLengthOfLongestColumn(
COL_HEADER_PAYMENT_METHOD.length(),
@ -147,7 +150,7 @@ public class TableFormat {
.collect(Collectors.joining("\n"));
}
static String formatPaymentAcctTbl(List<PaymentAccount> paymentAccounts) {
public static String formatPaymentAcctTbl(List<PaymentAccount> paymentAccounts) {
// Some column values might be longer than header, so we need to calculate them.
int nameColWidth = getLengthOfLongestColumn(
COL_HEADER_NAME.length(),
@ -163,7 +166,7 @@ public class TableFormat {
+ padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_UUID + COL_HEADER_DELIMITER + "\n";
String colDataFormat = "%-" + nameColWidth + "s" // left justify
+ " %" + COL_HEADER_CURRENCY.length() + "s" // right justify
+ " %-" + COL_HEADER_CURRENCY.length() + "s" // left justify
+ " %-" + paymentMethodColWidth + "s" // left justify
+ " %-" + COL_HEADER_UUID.length() + "s"; // left justify
return headerLine

View file

@ -25,6 +25,7 @@ import java.util.function.Supplier;
import static bisq.cli.ColumnHeaderConstants.*;
import static bisq.cli.CurrencyFormat.formatOfferPrice;
import static bisq.cli.CurrencyFormat.formatOfferVolume;
import static bisq.cli.CurrencyFormat.formatSatoshis;
import static com.google.common.base.Strings.padEnd;
@ -54,18 +55,33 @@ public class TradeFormat {
+ takerFeeHeaderFormat.get()
+ COL_HEADER_TRADE_DEPOSIT_PUBLISHED + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_DEPOSIT_CONFIRMED + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_FIAT_SENT + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_FIAT_RECEIVED + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_BUYER_COST + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_PAYMENT_SENT + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_PAYMENT_RECEIVED + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_PAYOUT_PUBLISHED + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_WITHDRAWN + COL_HEADER_DELIMITER
+ "%n";
String counterCurrencyCode = tradeInfo.getOffer().getCounterCurrencyCode();
String baseCurrencyCode = tradeInfo.getOffer().getBaseCurrencyCode();
String headerLine = isTaker
? String.format(headersFormat, counterCurrencyCode, baseCurrencyCode, baseCurrencyCode, baseCurrencyCode)
: String.format(headersFormat, counterCurrencyCode, baseCurrencyCode, baseCurrencyCode);
// The taker's output contains an extra taker tx fee column.
String headerLine = isTaker
? String.format(headersFormat,
/* COL_HEADER_PRICE */ counterCurrencyCode,
/* COL_HEADER_TRADE_AMOUNT */ baseCurrencyCode,
/* COL_HEADER_TRADE_TX_FEE */ baseCurrencyCode,
/* COL_HEADER_TRADE_TAKER_FEE */ baseCurrencyCode,
/* COL_HEADER_TRADE_BUYER_COST */ counterCurrencyCode,
/* COL_HEADER_TRADE_PAYMENT_SENT */ counterCurrencyCode,
/* COL_HEADER_TRADE_PAYMENT_RECEIVED */ counterCurrencyCode)
: String.format(headersFormat,
/* COL_HEADER_PRICE */ counterCurrencyCode,
/* COL_HEADER_TRADE_AMOUNT */ baseCurrencyCode,
/* COL_HEADER_TRADE_TX_FEE */ baseCurrencyCode,
/* COL_HEADER_TRADE_BUYER_COST */ counterCurrencyCode,
/* COL_HEADER_TRADE_PAYMENT_SENT */ counterCurrencyCode,
/* COL_HEADER_TRADE_PAYMENT_RECEIVED */ counterCurrencyCode);
String colDataFormat = "%-" + shortIdColWidth + "s" // lt justify
+ " %-" + (roleColWidth + COL_HEADER_DELIMITER.length()) + "s" // left
@ -75,8 +91,9 @@ public class TradeFormat {
+ takerFeeHeader.get() // rt justify
+ " %-" + COL_HEADER_TRADE_DEPOSIT_PUBLISHED.length() + "s" // lt justify
+ " %-" + COL_HEADER_TRADE_DEPOSIT_CONFIRMED.length() + "s" // lt justify
+ " %-" + COL_HEADER_TRADE_FIAT_SENT.length() + "s" // lt justify
+ " %-" + COL_HEADER_TRADE_FIAT_RECEIVED.length() + "s" // lt justify
+ "%" + (COL_HEADER_TRADE_BUYER_COST.length() + 1) + "s" // rt justify
+ " %-" + (COL_HEADER_TRADE_PAYMENT_SENT.length() - 1) + "s" // left
+ " %-" + (COL_HEADER_TRADE_PAYMENT_RECEIVED.length() - 1) + "s" // left
+ " %-" + COL_HEADER_TRADE_PAYOUT_PUBLISHED.length() + "s" // lt justify
+ " %-" + COL_HEADER_TRADE_WITHDRAWN.length() + "s"; // lt justify
@ -95,6 +112,7 @@ public class TradeFormat {
formatSatoshis(tradeInfo.getTxFeeAsLong()),
tradeInfo.getIsDepositPublished() ? "YES" : "NO",
tradeInfo.getIsDepositConfirmed() ? "YES" : "NO",
formatOfferVolume(tradeInfo.getOffer().getVolume()),
tradeInfo.getIsFiatSent() ? "YES" : "NO",
tradeInfo.getIsFiatReceived() ? "YES" : "NO",
tradeInfo.getIsPayoutPublished() ? "YES" : "NO",
@ -111,6 +129,7 @@ public class TradeFormat {
formatSatoshis(tradeInfo.getTakerFeeAsLong()),
tradeInfo.getIsDepositPublished() ? "YES" : "NO",
tradeInfo.getIsDepositConfirmed() ? "YES" : "NO",
formatOfferVolume(tradeInfo.getOffer().getVolume()),
tradeInfo.getIsFiatSent() ? "YES" : "NO",
tradeInfo.getIsFiatReceived() ? "YES" : "NO",
tradeInfo.getIsPayoutPublished() ? "YES" : "NO",

View file

@ -17,11 +17,13 @@
package bisq.cli.opts;
import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import java.util.List;
import java.util.function.Function;
import lombok.Getter;
@ -48,12 +50,29 @@ abstract class AbstractMethodOptionParser implements MethodOpts {
}
public AbstractMethodOptionParser parse() {
options = parser.parse(new ArgumentList(args).getMethodArguments());
nonOptionArguments = (List<String>) options.nonOptionArguments();
return this;
try {
options = parser.parse(new ArgumentList(args).getMethodArguments());
//noinspection unchecked
nonOptionArguments = (List<String>) options.nonOptionArguments();
return this;
} catch (OptionException ex) {
throw new IllegalArgumentException(cliExceptionMessageStyle.apply(ex), ex);
}
}
public boolean isForHelp() {
return options.has(helpOpt);
}
private final Function<OptionException, String> cliExceptionMessageStyle = (ex) -> {
if (ex.getMessage() == null)
return null;
var optionToken = "option ";
var cliMessage = ex.getMessage().toLowerCase();
if (cliMessage.startsWith(optionToken) && cliMessage.length() > optionToken.length()) {
cliMessage = cliMessage.substring(cliMessage.indexOf(" ") + 1);
}
return cliMessage;
};
}

View file

@ -21,13 +21,11 @@ package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_OFFER_ID;
import static joptsimple.internal.Strings.EMPTY;
public class CancelOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to cancel")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
public CancelOfferOptionParser(String[] args) {
super(args);
@ -40,7 +38,7 @@ public class CancelOfferOptionParser extends AbstractMethodOptionParser implemen
if (options.has(helpOpt))
return this;
if (!options.has(offerIdOpt))
if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty())
throw new IllegalArgumentException("no offer id specified");
return this;

View file

@ -33,20 +33,16 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implemen
.defaultsTo(EMPTY);
final OptionSpec<String> directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (eur|usd|...)")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<String> amountOpt = parser.accepts(OPT_AMOUNT, "amount of btc to buy or sell")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<String> minAmountOpt = parser.accepts(OPT_MIN_AMOUNT, "minimum amount of btc to buy or sell")
.withOptionalArg()
.defaultsTo(EMPTY);
.withOptionalArg();
final OptionSpec<String> mktPriceMarginOpt = parser.accepts(OPT_MKT_PRICE_MARGIN, "market btc price margin (%)")
.withOptionalArg()
@ -54,11 +50,10 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implemen
final OptionSpec<String> fixedPriceOpt = parser.accepts(OPT_FIXED_PRICE, "fixed btc price")
.withOptionalArg()
.defaultsTo(EMPTY);
.defaultsTo("0");
final OptionSpec<String> securityDepositOpt = parser.accepts(OPT_SECURITY_DEPOSIT, "maker security deposit (%)")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<String> makerFeeCurrencyCodeOpt = parser.accepts(OPT_FEE_CURRENCY, "maker fee currency code (bsq|btc)")
.withOptionalArg()
@ -75,19 +70,28 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implemen
if (options.has(helpOpt))
return this;
if (!options.has(paymentAccountIdOpt))
if (!options.has(paymentAccountIdOpt) || options.valueOf(paymentAccountIdOpt).isEmpty())
throw new IllegalArgumentException("no payment account id specified");
if (!options.has(directionOpt))
if (!options.has(directionOpt) || options.valueOf(directionOpt).isEmpty())
throw new IllegalArgumentException("no direction (buy|sell) specified");
if (!options.has(amountOpt))
if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty())
throw new IllegalArgumentException("no currency code specified");
if (!options.has(amountOpt) || options.valueOf(amountOpt).isEmpty())
throw new IllegalArgumentException("no btc amount specified");
if (!options.has(mktPriceMarginOpt) && !options.has(fixedPriceOpt))
throw new IllegalArgumentException("no market price margin or fixed price specified");
if (!options.has(securityDepositOpt))
if (options.has(mktPriceMarginOpt) && options.valueOf(mktPriceMarginOpt).isEmpty())
throw new IllegalArgumentException("no market price margin specified");
if (options.has(fixedPriceOpt) && options.valueOf(fixedPriceOpt).isEmpty())
throw new IllegalArgumentException("no fixed price specified");
if (!options.has(securityDepositOpt) || options.valueOf(securityDepositOpt).isEmpty())
throw new IllegalArgumentException("no security deposit specified");
return this;

View file

@ -25,14 +25,12 @@ import java.nio.file.Paths;
import static bisq.cli.opts.OptLabel.OPT_PAYMENT_ACCOUNT_FORM;
import static java.lang.String.format;
import static joptsimple.internal.Strings.EMPTY;
public class CreatePaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> paymentAcctFormPathOpt = parser.accepts(OPT_PAYMENT_ACCOUNT_FORM,
"path to json payment account form")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
public CreatePaymentAcctOptionParser(String[] args) {
super(args);
@ -45,7 +43,7 @@ public class CreatePaymentAcctOptionParser extends AbstractMethodOptionParser im
if (options.has(helpOpt))
return this;
if (!options.has(paymentAcctFormPathOpt))
if (!options.has(paymentAcctFormPathOpt) || options.valueOf(paymentAcctFormPathOpt).isEmpty())
throw new IllegalArgumentException("no path to json payment account form specified");
Path path = Paths.get(options.valueOf(paymentAcctFormPathOpt));

View file

@ -21,13 +21,11 @@ package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_ADDRESS;
import static joptsimple.internal.Strings.EMPTY;
public class GetAddressBalanceOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> addressOpt = parser.accepts(OPT_ADDRESS, "wallet btc address")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
public GetAddressBalanceOptionParser(String[] args) {
super(args);
@ -40,7 +38,7 @@ public class GetAddressBalanceOptionParser extends AbstractMethodOptionParser im
if (options.has(helpOpt))
return this;
if (!options.has(addressOpt))
if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty())
throw new IllegalArgumentException("no address specified");
return this;

View file

@ -0,0 +1,50 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE;
public class GetBTCMarketPriceOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency-code")
.withRequiredArg();
public GetBTCMarketPriceOptionParser(String[] args) {
super(args);
}
public GetBTCMarketPriceOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty())
throw new IllegalArgumentException("no currency code specified");
return this;
}
public String getCurrencyCode() {
return options.valueOf(currencyCodeOpt);
}
}

View file

@ -21,13 +21,11 @@ package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_OFFER_ID;
import static joptsimple.internal.Strings.EMPTY;
public class GetOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to get")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
public GetOfferOptionParser(String[] args) {
super(args);
@ -40,7 +38,7 @@ public class GetOfferOptionParser extends AbstractMethodOptionParser implements
if (options.has(helpOpt))
return this;
if (!options.has(offerIdOpt))
if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty())
throw new IllegalArgumentException("no offer id specified");
return this;

View file

@ -22,17 +22,14 @@ import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE;
import static bisq.cli.opts.OptLabel.OPT_DIRECTION;
import static joptsimple.internal.Strings.EMPTY;
public class GetOffersOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (eur|usd|...)")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
public GetOffersOptionParser(String[] args) {
super(args);
@ -45,10 +42,10 @@ public class GetOffersOptionParser extends AbstractMethodOptionParser implements
if (options.has(helpOpt))
return this;
if (!options.has(directionOpt))
if (!options.has(directionOpt) || options.valueOf(directionOpt).isEmpty())
throw new IllegalArgumentException("no direction (buy|sell) specified");
if (!options.has(currencyCodeOpt))
if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty())
throw new IllegalArgumentException("no currency code specified");
return this;

View file

@ -21,14 +21,12 @@ package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_PAYMENT_METHOD_ID;
import static joptsimple.internal.Strings.EMPTY;
public class GetPaymentAcctFormOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> paymentMethodIdOpt = parser.accepts(OPT_PAYMENT_METHOD_ID,
"id of payment method type used by a payment account")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
public GetPaymentAcctFormOptionParser(String[] args) {
super(args);
@ -41,7 +39,7 @@ public class GetPaymentAcctFormOptionParser extends AbstractMethodOptionParser i
if (options.has(helpOpt))
return this;
if (!options.has(paymentMethodIdOpt))
if (!options.has(paymentMethodIdOpt) || options.valueOf(paymentMethodIdOpt).isEmpty())
throw new IllegalArgumentException("no payment method id specified");
return this;

View file

@ -22,13 +22,11 @@ import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_SHOW_CONTRACT;
import static bisq.cli.opts.OptLabel.OPT_TRADE_ID;
import static joptsimple.internal.Strings.EMPTY;
public class GetTradeOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> tradeIdOpt = parser.accepts(OPT_TRADE_ID, "id of trade")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<Boolean> showContractOpt = parser.accepts(OPT_SHOW_CONTRACT, "show trade's json contract")
.withOptionalArg()
@ -46,7 +44,7 @@ public class GetTradeOptionParser extends AbstractMethodOptionParser implements
if (options.has(helpOpt))
return this;
if (!options.has(tradeIdOpt))
if (!options.has(tradeIdOpt) || options.valueOf(tradeIdOpt).isEmpty())
throw new IllegalArgumentException("no trade id specified");
return this;

View file

@ -21,13 +21,11 @@ package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_TRANSACTION_ID;
import static joptsimple.internal.Strings.EMPTY;
public class GetTransactionOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> txIdOpt = parser.accepts(OPT_TRANSACTION_ID, "id of transaction")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
public GetTransactionOptionParser(String[] args) {
super(args);
@ -40,7 +38,7 @@ public class GetTransactionOptionParser extends AbstractMethodOptionParser imple
if (options.has(helpOpt))
return this;
if (!options.has(txIdOpt))
if (!options.has(txIdOpt) || options.valueOf(txIdOpt).isEmpty())
throw new IllegalArgumentException("no tx id specified");
return this;

View file

@ -22,17 +22,14 @@ import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_DISPUTE_AGENT_TYPE;
import static bisq.cli.opts.OptLabel.OPT_REGISTRATION_KEY;
import static joptsimple.internal.Strings.EMPTY;
public class RegisterDisputeAgentOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> disputeAgentTypeOpt = parser.accepts(OPT_DISPUTE_AGENT_TYPE, "dispute agent type")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<String> registrationKeyOpt = parser.accepts(OPT_REGISTRATION_KEY, "registration key")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
public RegisterDisputeAgentOptionParser(String[] args) {
super(args);
@ -45,10 +42,10 @@ public class RegisterDisputeAgentOptionParser extends AbstractMethodOptionParser
if (options.has(helpOpt))
return this;
if (!options.has(disputeAgentTypeOpt))
if (!options.has(disputeAgentTypeOpt) || options.valueOf(disputeAgentTypeOpt).isEmpty())
throw new IllegalArgumentException("no dispute agent type specified");
if (!options.has(registrationKeyOpt))
if (!options.has(registrationKeyOpt) || options.valueOf(registrationKeyOpt).isEmpty())
throw new IllegalArgumentException("no registration key specified");
return this;

View file

@ -21,13 +21,11 @@ package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_WALLET_PASSWORD;
import static joptsimple.internal.Strings.EMPTY;
public class RemoveWalletPasswordOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> passwordOpt = parser.accepts(OPT_WALLET_PASSWORD, "bisq wallet password")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
public RemoveWalletPasswordOptionParser(String[] args) {
super(args);
@ -40,7 +38,7 @@ public class RemoveWalletPasswordOptionParser extends AbstractMethodOptionParser
if (options.has(helpOpt))
return this;
if (!options.has(passwordOpt))
if (!options.has(passwordOpt) || options.valueOf(passwordOpt).isEmpty())
throw new IllegalArgumentException("no password specified");
return this;

View file

@ -28,12 +28,10 @@ import static joptsimple.internal.Strings.EMPTY;
public class SendBsqOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> addressOpt = parser.accepts(OPT_ADDRESS, "destination bsq address")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<String> amountOpt = parser.accepts(OPT_AMOUNT, "amount of bsq to send")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<String> feeRateOpt = parser.accepts(OPT_TX_FEE_RATE, "optional tx fee rate (sats/byte)")
.withOptionalArg()
@ -50,10 +48,10 @@ public class SendBsqOptionParser extends AbstractMethodOptionParser implements M
if (options.has(helpOpt))
return this;
if (!options.has(addressOpt))
if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty())
throw new IllegalArgumentException("no bsq address specified");
if (!options.has(amountOpt))
if (!options.has(amountOpt) || options.valueOf(amountOpt).isEmpty())
throw new IllegalArgumentException("no bsq amount specified");
return this;

View file

@ -29,12 +29,10 @@ import static joptsimple.internal.Strings.EMPTY;
public class SendBtcOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> addressOpt = parser.accepts(OPT_ADDRESS, "destination btc address")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<String> amountOpt = parser.accepts(OPT_AMOUNT, "amount of btc to send")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<String> feeRateOpt = parser.accepts(OPT_TX_FEE_RATE, "optional tx fee rate (sats/byte)")
.withOptionalArg()
@ -55,10 +53,10 @@ public class SendBtcOptionParser extends AbstractMethodOptionParser implements M
if (options.has(helpOpt))
return this;
if (!options.has(addressOpt))
if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty())
throw new IllegalArgumentException("no btc address specified");
if (!options.has(amountOpt))
if (!options.has(amountOpt) || options.valueOf(amountOpt).isEmpty())
throw new IllegalArgumentException("no btc amount specified");
return this;

View file

@ -21,14 +21,12 @@ package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_TX_FEE_RATE;
import static joptsimple.internal.Strings.EMPTY;
public class SetTxFeeRateOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> feeRateOpt = parser.accepts(OPT_TX_FEE_RATE,
"tx fee rate preference (sats/byte)")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
public SetTxFeeRateOptionParser(String[] args) {
super(args);
@ -41,7 +39,7 @@ public class SetTxFeeRateOptionParser extends AbstractMethodOptionParser impleme
if (options.has(helpOpt))
return this;
if (!options.has(feeRateOpt))
if (!options.has(feeRateOpt) || options.valueOf(feeRateOpt).isEmpty())
throw new IllegalArgumentException("no tx fee rate specified");
return this;

View file

@ -27,8 +27,7 @@ import static joptsimple.internal.Strings.EMPTY;
public class SetWalletPasswordOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> passwordOpt = parser.accepts(OPT_WALLET_PASSWORD, "bisq wallet password")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<String> newPasswordOpt = parser.accepts(OPT_NEW_WALLET_PASSWORD, "new bisq wallet password")
.withOptionalArg()
@ -45,7 +44,7 @@ public class SetWalletPasswordOptionParser extends AbstractMethodOptionParser im
if (options.has(helpOpt))
return this;
if (!options.has(passwordOpt))
if (!options.has(passwordOpt) || options.valueOf(passwordOpt).isEmpty())
throw new IllegalArgumentException("no password specified");
return this;

View file

@ -23,17 +23,14 @@ import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_FEE_CURRENCY;
import static bisq.cli.opts.OptLabel.OPT_OFFER_ID;
import static bisq.cli.opts.OptLabel.OPT_PAYMENT_ACCOUNT;
import static joptsimple.internal.Strings.EMPTY;
public class TakeOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to take")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<String> paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT, "id of payment account used for trade")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<String> takerFeeCurrencyCodeOpt = parser.accepts(OPT_FEE_CURRENCY, "taker fee currency code (bsq|btc)")
.withOptionalArg()
@ -50,10 +47,10 @@ public class TakeOfferOptionParser extends AbstractMethodOptionParser implements
if (options.has(helpOpt))
return this;
if (!options.has(offerIdOpt))
if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty())
throw new IllegalArgumentException("no offer id specified");
if (!options.has(paymentAccountIdOpt))
if (!options.has(paymentAccountIdOpt) || options.valueOf(paymentAccountIdOpt).isEmpty())
throw new IllegalArgumentException("no payment account id specified");
return this;

View file

@ -22,13 +22,11 @@ import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_TIMEOUT;
import static bisq.cli.opts.OptLabel.OPT_WALLET_PASSWORD;
import static joptsimple.internal.Strings.EMPTY;
public class UnlockWalletOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> passwordOpt = parser.accepts(OPT_WALLET_PASSWORD, "bisq wallet password")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<Long> unlockTimeoutOpt = parser.accepts(OPT_TIMEOUT, "wallet unlock timeout (s)")
.withRequiredArg()
@ -46,7 +44,7 @@ public class UnlockWalletOptionParser extends AbstractMethodOptionParser impleme
if (options.has(helpOpt))
return this;
if (!options.has(passwordOpt))
if (!options.has(passwordOpt) || options.valueOf(passwordOpt).isEmpty())
throw new IllegalArgumentException("no password specified");
if (!options.has(unlockTimeoutOpt) || options.valueOf(unlockTimeoutOpt) <= 0)

View file

@ -27,13 +27,11 @@ import static joptsimple.internal.Strings.EMPTY;
public class WithdrawFundsOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> tradeIdOpt = parser.accepts(OPT_TRADE_ID, "id of trade to get")
.withRequiredArg()
.defaultsTo(EMPTY);
final OptionSpec<String> tradeIdOpt = parser.accepts(OPT_TRADE_ID, "id of trade")
.withRequiredArg();
final OptionSpec<String> addressOpt = parser.accepts(OPT_ADDRESS, "destination btc address")
.withRequiredArg()
.defaultsTo(EMPTY);
.withRequiredArg();
final OptionSpec<String> memoOpt = parser.accepts(OPT_MEMO, "optional tx memo")
.withOptionalArg()
@ -50,9 +48,12 @@ public class WithdrawFundsOptionParser extends AbstractMethodOptionParser implem
if (options.has(helpOpt))
return this;
if (!options.has(tradeIdOpt))
if (!options.has(tradeIdOpt) || options.valueOf(tradeIdOpt).isEmpty())
throw new IllegalArgumentException("no trade id specified");
if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty())
throw new IllegalArgumentException("no destination address specified");
return this;
}

View file

@ -16,24 +16,24 @@ public class GetOffersSmokeTest {
public static void main(String[] args) {
out.println(">>> getoffers buy usd");
CliMain.main(new String[]{"--password=xyz", "getoffers", "buy", "usd"});
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=usd"});
out.println(">>> getoffers sell usd");
CliMain.main(new String[]{"--password=xyz", "getoffers", "sell", "usd"});
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=usd"});
out.println(">>> getoffers buy eur");
CliMain.main(new String[]{"--password=xyz", "getoffers", "buy", "eur"});
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=eur"});
out.println(">>> getoffers sell eur");
CliMain.main(new String[]{"--password=xyz", "getoffers", "sell", "eur"});
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=eur"});
out.println(">>> getoffers buy gbp");
CliMain.main(new String[]{"--password=xyz", "getoffers", "buy", "gbp"});
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=gbp"});
out.println(">>> getoffers sell gbp");
CliMain.main(new String[]{"--password=xyz", "getoffers", "sell", "gbp"});
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=gbp"});
out.println(">>> getoffers buy brl");
CliMain.main(new String[]{"--password=xyz", "getoffers", "buy", "brl"});
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=brl"});
out.println(">>> getoffers sell brl");
CliMain.main(new String[]{"--password=xyz", "getoffers", "sell", "brl"});
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=brl"});
}
}

View file

@ -0,0 +1,180 @@
package bisq.cli.opt;
import org.junit.jupiter.api.Test;
import static bisq.cli.Method.canceloffer;
import static bisq.cli.Method.createoffer;
import static bisq.cli.Method.createpaymentacct;
import static bisq.cli.opts.OptLabel.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import bisq.cli.opts.CancelOfferOptionParser;
import bisq.cli.opts.CreateOfferOptionParser;
import bisq.cli.opts.CreatePaymentAcctOptionParser;
public class OptionParsersTest {
private static final String PASSWORD_OPT = "--" + OPT_PASSWORD + "=" + "xyz";
// CancelOffer opt parsing tests
@Test
public void testCancelOfferWithMissingOfferIdOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
canceloffer.name()
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CancelOfferOptionParser(args).parse());
assertEquals("no offer id specified", exception.getMessage());
}
@Test
public void testCancelOfferWithEmptyOfferIdOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
canceloffer.name(),
"--" + OPT_OFFER_ID + "=" // missing opt value
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CancelOfferOptionParser(args).parse());
assertEquals("no offer id specified", exception.getMessage());
}
@Test
public void testCancelOfferWithMissingOfferIdValueShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
canceloffer.name(),
"--" + OPT_OFFER_ID // missing equals sign & opt value
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CancelOfferOptionParser(args).parse());
assertEquals("offer-id requires an argument", exception.getMessage());
}
@Test
public void testValidCancelOfferOpts() {
String[] args = new String[]{
PASSWORD_OPT,
canceloffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID"
};
new CancelOfferOptionParser(args).parse();
}
// CreateOffer opt parsing tests
@Test
public void testCreateOfferOptParserWithMissingPaymentAccountIdOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createoffer.name()
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreateOfferOptionParser(args).parse());
assertEquals("no payment account id specified", exception.getMessage());
}
@Test
public void testCreateOfferOptParserWithEmptyPaymentAccountIdOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createoffer.name(),
"--" + OPT_PAYMENT_ACCOUNT
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreateOfferOptionParser(args).parse());
assertEquals("payment-account requires an argument", exception.getMessage());
}
@Test
public void testCreateOfferOptParserWithMissingDirectionOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createoffer.name(),
"--" + OPT_PAYMENT_ACCOUNT + "=" + "abc-payment-acct-id-123"
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreateOfferOptionParser(args).parse());
assertEquals("no direction (buy|sell) specified", exception.getMessage());
}
@Test
public void testCreateOfferOptParserWithMissingDirectionOptValueShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createoffer.name(),
"--" + OPT_PAYMENT_ACCOUNT + "=" + "abc-payment-acct-id-123",
"--" + OPT_DIRECTION + "=" + ""
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreateOfferOptionParser(args).parse());
assertEquals("no direction (buy|sell) specified", exception.getMessage());
}
@Test
public void testValidCreateOfferOpts() {
String[] args = new String[]{
PASSWORD_OPT,
createoffer.name(),
"--" + OPT_PAYMENT_ACCOUNT + "=" + "abc-payment-acct-id-123",
"--" + OPT_DIRECTION + "=" + "BUY",
"--" + OPT_CURRENCY_CODE + "=" + "EUR",
"--" + OPT_AMOUNT + "=" + "0.125",
"--" + OPT_MKT_PRICE_MARGIN + "=" + "0.0",
"--" + OPT_SECURITY_DEPOSIT + "=" + "25.0"
};
CreateOfferOptionParser parser = new CreateOfferOptionParser(args).parse();
assertEquals("abc-payment-acct-id-123", parser.getPaymentAccountId());
assertEquals("BUY", parser.getDirection());
assertEquals("EUR", parser.getCurrencyCode());
assertEquals("0.125", parser.getAmount());
assertEquals("0.0", parser.getMktPriceMargin());
assertEquals("25.0", parser.getSecurityDeposit());
}
// CreatePaymentAcct opt parser tests
@Test
public void testCreatePaymentAcctOptParserWithMissingPaymentFormOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createpaymentacct.name()
// OPT_PAYMENT_ACCOUNT_FORM
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreatePaymentAcctOptionParser(args).parse());
assertEquals("no path to json payment account form specified", exception.getMessage());
}
@Test
public void testCreatePaymentAcctOptParserWithMissingPaymentFormOptValueShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createpaymentacct.name(),
"--" + OPT_PAYMENT_ACCOUNT_FORM + "="
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreatePaymentAcctOptionParser(args).parse());
assertEquals("no path to json payment account form specified", exception.getMessage());
}
@Test
public void testCreatePaymentAcctOptParserWithInvalidPaymentFormOptValueShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createpaymentacct.name(),
"--" + OPT_PAYMENT_ACCOUNT_FORM + "=" + "/tmp/milkyway/solarsystem/mars"
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreatePaymentAcctOptionParser(args).parse());
assertEquals("json payment account form '/tmp/milkyway/solarsystem/mars' could not be found",
exception.getMessage());
}
}

View file

@ -30,14 +30,14 @@ public class Version {
// VERSION = 0.5.0 introduces proto buffer for the P2P network and local DB and is a not backward compatible update
// Therefore all sub versions start again with 1
// We use semantic versioning with major, minor and patch
public static final String VERSION = "1.5.4";
public static final String VERSION = "1.6.0";
/**
* Holds a list of the tagged resource files for optimizing the getData requests.
* This must not contain each version but only those where we add new version-tagged resource files for
* historical data stores.
*/
public static final List<String> HISTORICAL_RESOURCE_FILE_VERSION_TAGS = Arrays.asList("1.4.0", "1.5.0", "1.5.2");
public static final List<String> HISTORICAL_RESOURCE_FILE_VERSION_TAGS = Arrays.asList("1.4.0", "1.5.0", "1.5.2", "1.5.5", "1.5.7");
public static int getMajorVersion(String version) {
return getSubVersion(version, 0);

View file

@ -73,6 +73,6 @@ public enum BaseCurrencyNetwork {
}
public long getDefaultMinFeePerVbyte() {
return 2;
return 15; // 2021-02-22 due to mempool congestion, increased from 2
}
}

View file

@ -121,6 +121,10 @@ public class Config {
public static final String API_PORT = "apiPort";
public static final String PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE = "preventPeriodicShutdownAtSeedNode";
public static final String REPUBLISH_MAILBOX_ENTRIES = "republishMailboxEntries";
public static final String BTC_TX_FEE = "btcTxFee";
public static final String BTC_MIN_TX_FEE = "btcMinTxFee";
public static final String BTC_FEES_TS = "bitcoinFeesTs";
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
// Default values for certain options
public static final int UNSPECIFIED_PORT = -1;
@ -209,6 +213,7 @@ public class Config {
public final int apiPort;
public final boolean preventPeriodicShutdownAtSeedNode;
public final boolean republishMailboxEntries;
public final boolean bypassMempoolValidation;
// Properties derived from options but not exposed as options themselves
public final File torDir;
@ -657,6 +662,13 @@ public class Config {
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> bypassMempoolValidationOpt =
parser.accepts(BYPASS_MEMPOOL_VALIDATION,
"Prevents mempool check of trade parameters")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
try {
CompositeOptionSet options = new CompositeOptionSet();
@ -774,6 +786,7 @@ public class Config {
this.apiPort = options.valueOf(apiPortOpt);
this.preventPeriodicShutdownAtSeedNode = options.valueOf(preventPeriodicShutdownAtSeedNodeOpt);
this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt);
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
} catch (OptionException ex) {
throw new ConfigException("problem parsing option '%s': %s",
ex.options().get(0),

View file

@ -81,24 +81,53 @@ public class PersistenceManager<T extends PersistableEnvelope> {
///////////////////////////////////////////////////////////////////////////////////////////
public static final Map<String, PersistenceManager<?>> ALL_PERSISTENCE_MANAGERS = new HashMap<>();
public static boolean FLUSH_ALL_DATA_TO_DISK_CALLED = false;
private static boolean flushAtShutdownCalled;
private static final AtomicBoolean allServicesInitialized = new AtomicBoolean(false);
public static void onAllServicesInitialized() {
allServicesInitialized.set(true);
ALL_PERSISTENCE_MANAGERS.values().forEach(persistenceManager -> {
// In case we got a requestPersistence call before we got initialized we trigger the timer for the
// persist call
if (persistenceManager.persistenceRequested) {
persistenceManager.maybeStartTimerForPersistence();
}
});
}
public static void flushAllDataToDiskAtBackup(ResultHandler completeHandler) {
flushAllDataToDisk(completeHandler, false);
}
public static void flushAllDataToDiskAtShutdown(ResultHandler completeHandler) {
flushAllDataToDisk(completeHandler, true);
}
// We require being called only once from the global shutdown routine. As the shutdown routine has a timeout
// and error condition where we call the method as well beside the standard path and it could be that those
// alternative code paths call our method after it was called already, so it is a valid but rare case.
// We add a guard to prevent repeated calls.
public static void flushAllDataToDisk(ResultHandler completeHandler) {
private static void flushAllDataToDisk(ResultHandler completeHandler, boolean doShutdown) {
if (!allServicesInitialized.get()) {
log.warn("Application has not completed start up yet so we do not flush data to disk.");
completeHandler.handleResult();
return;
}
// We don't know from which thread we are called so we map to user thread
UserThread.execute(() -> {
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
log.warn("We got flushAllDataToDisk called again. This can happen in some rare cases. We ignore the repeated call.");
return;
if (doShutdown) {
if (flushAtShutdownCalled) {
log.warn("We got flushAllDataToDisk called again. This can happen in some rare cases. We ignore the repeated call.");
return;
}
flushAtShutdownCalled = true;
}
FLUSH_ALL_DATA_TO_DISK_CALLED = true;
log.info("Start flushAllDataToDisk at shutdown");
log.info("Start flushAllDataToDisk");
AtomicInteger openInstances = new AtomicInteger(ALL_PERSISTENCE_MANAGERS.size());
if (openInstances.get() == 0) {
@ -120,9 +149,9 @@ public class PersistenceManager<T extends PersistableEnvelope> {
// We get our result handler called from the write thread so we map back to user thread.
persistenceManager.persistNow(() ->
UserThread.execute(() -> onWriteCompleted(completeHandler, openInstances, persistenceManager)));
UserThread.execute(() -> onWriteCompleted(completeHandler, openInstances, persistenceManager, doShutdown)));
} else {
onWriteCompleted(completeHandler, openInstances, persistenceManager);
onWriteCompleted(completeHandler, openInstances, persistenceManager, doShutdown);
}
});
});
@ -131,13 +160,16 @@ public class PersistenceManager<T extends PersistableEnvelope> {
// We get called always from user thread here.
private static void onWriteCompleted(ResultHandler completeHandler,
AtomicInteger openInstances,
PersistenceManager<?> persistenceManager) {
persistenceManager.shutdown();
PersistenceManager<?> persistenceManager,
boolean doShutdown) {
if (doShutdown) {
persistenceManager.shutdown();
}
if (openInstances.decrementAndGet() == 0) {
log.info("flushAllDataToDisk completed");
completeHandler.handleResult();
}
}
@ -213,7 +245,7 @@ public class PersistenceManager<T extends PersistableEnvelope> {
}
public void initialize(T persistable, String fileName, Source source) {
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
if (flushAtShutdownCalled) {
log.warn("We have started the shut down routine already. We ignore that initialize call.");
return;
}
@ -279,7 +311,7 @@ public class PersistenceManager<T extends PersistableEnvelope> {
* @param orElse Called if no file exists or reading of file failed.
*/
public void readPersisted(String fileName, Consumer<T> resultHandler, Runnable orElse) {
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
if (flushAtShutdownCalled) {
log.warn("We have started the shut down routine already. We ignore that readPersisted call.");
return;
}
@ -303,7 +335,7 @@ public class PersistenceManager<T extends PersistableEnvelope> {
@Nullable
public T getPersisted(String fileName) {
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
if (flushAtShutdownCalled) {
log.warn("We have started the shut down routine already. We ignore that getPersisted call.");
return null;
}
@ -346,13 +378,23 @@ public class PersistenceManager<T extends PersistableEnvelope> {
///////////////////////////////////////////////////////////////////////////////////////////
public void requestPersistence() {
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
if (flushAtShutdownCalled) {
log.warn("We have started the shut down routine already. We ignore that requestPersistence call.");
return;
}
persistenceRequested = true;
// If we have not initialized yet we postpone the start of the timer and call maybeStartTimerForPersistence at
// onAllServicesInitialized
if (!allServicesInitialized.get()) {
return;
}
maybeStartTimerForPersistence();
}
private void maybeStartTimerForPersistence() {
// We write to disk with a delay to avoid frequent write operations. Depending on the priority those delays
// can be rather long.
if (timer == null) {
@ -387,6 +429,12 @@ public class PersistenceManager<T extends PersistableEnvelope> {
}
public void writeToDisk(protobuf.PersistableEnvelope serialized, @Nullable Runnable completeHandler) {
if (!allServicesInitialized.get()) {
log.warn("Application has not completed start up yet so we do not permit writing data to disk.");
UserThread.execute(completeHandler);
return;
}
long ts = System.currentTimeMillis();
File tempFile = null;
FileOutputStream fileOutputStream = null;
@ -459,7 +507,6 @@ public class PersistenceManager<T extends PersistableEnvelope> {
return writeToDiskExecutor;
}
@Override
public String toString() {
return "PersistenceManager{" +

View file

@ -63,7 +63,7 @@ class DesktopUtil {
}
if (os.isWindows()) {
return runCommand("explorer", "%s", what);
return runCommand("explorer", "%s", "\"" + what + "\"");
}
return false;
@ -94,7 +94,7 @@ class DesktopUtil {
return true;
}
} catch (IOException e) {
log.warn("Error running command. {}", e);
log.warn("Error running command. {}", e.toString());
return false;
}
}

View file

@ -28,9 +28,13 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import lombok.extern.slf4j.Slf4j;
import static java.lang.String.format;
import static java.util.Arrays.stream;
import static org.apache.commons.lang3.StringUtils.capitalize;
@Slf4j
public class ReflectionUtils {
/**
@ -105,4 +109,32 @@ public class ReflectionUtils {
else
return "";
}
public static Field getField(String name, Class<?> clazz) {
Optional<Field> field = stream(clazz.getDeclaredFields())
.filter(f -> f.getName().equals(name)).findFirst();
return field.orElseThrow(() ->
new IllegalArgumentException(format("field %s not found in class %s",
name,
clazz.getSimpleName())));
}
public static Method getMethod(String name, Class<?> clazz) {
Optional<Method> method = stream(clazz.getDeclaredMethods())
.filter(m -> m.getName().equals(name)).findFirst();
return method.orElseThrow(() ->
new IllegalArgumentException(format("method %s not found in class %s",
name,
clazz.getSimpleName())));
}
public static void handleSetFieldValueError(Object object,
Field field,
ReflectiveOperationException ex) {
String errMsg = format("cannot set value of field %s, on class %s",
field.getName(),
object.getClass().getSimpleName());
log.error(capitalize(errMsg) + ".", ex);
throw new IllegalStateException("programmer error: " + errMsg);
}
}

View file

@ -414,14 +414,16 @@ public class AccountAgeWitnessService {
AccountAgeWitness accountAgeWitness,
AccountAge accountAgeCategory,
OfferPayload.Direction direction,
PaymentMethod paymentMethod) {
PaymentMethod paymentMethod,
boolean isMyLimit) {
if (CurrencyUtil.isCryptoCurrency(currencyCode) ||
!PaymentMethod.hasChargebackRisk(paymentMethod, currencyCode) ||
direction == OfferPayload.Direction.SELL) {
return maxTradeLimit.value;
}
long limit = OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value;
long limit = isMyLimit ? OfferRestrictions.TOLERATED_SMALL_AMOUNT_SELF.value :
OfferRestrictions.TOLERATED_SMALL_AMOUNT_PEER.value;
var factor = signedBuyFactor(accountAgeCategory);
if (factor > 0) {
limit = MathUtils.roundDoubleToLong((double) maxTradeLimit.value * factor);
@ -509,7 +511,8 @@ public class AccountAgeWitnessService {
accountAgeWitness,
accountAgeCategory,
direction,
paymentAccount.getPaymentMethod());
paymentAccount.getPaymentMethod(),
true);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -571,14 +574,14 @@ public class AccountAgeWitnessService {
checkNotNull(offer);
// In case we don't find the witness we check if the trade amount is above the
// TOLERATED_SMALL_TRADE_AMOUNT (0.01 BTC) and only in that case return false.
// TOLERATED_SMALL_AMOUNT_PEER and only in that case return false.
return findWitness(offer)
.map(witness -> verifyPeersTradeLimit(offer, tradeAmount, witness, new Date(), errorMessageHandler))
.orElse(isToleratedSmalleAmount(tradeAmount));
.orElse(isPeerToleratedSmallAmount(tradeAmount));
}
private boolean isToleratedSmalleAmount(Coin tradeAmount) {
return tradeAmount.value <= OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value;
private boolean isPeerToleratedSmallAmount(Coin tradeAmount) {
return tradeAmount.value <= OfferRestrictions.TOLERATED_SMALL_AMOUNT_PEER.value;
}
@ -642,7 +645,7 @@ public class AccountAgeWitnessService {
OfferPayload.Direction direction = offer.isMyOffer(keyRing) ?
offer.getMirroredDirection() : offer.getDirection();
peersCurrentTradeLimit = getTradeLimit(defaultMaxTradeLimit, currencyCode, peersWitness,
accountAgeCategory, direction, offer.getPaymentMethod());
accountAgeCategory, direction, offer.getPaymentMethod(), false);
}
// Makers current trade limit cannot be smaller than that in the offer
boolean result = tradeAmount.value <= peersCurrentTradeLimit;

View file

@ -17,6 +17,8 @@
package bisq.core.alert;
import bisq.core.user.Preferences;
import bisq.network.p2p.storage.payload.ExpirablePayload;
import bisq.network.p2p.storage.payload.ProtectedStoragePayload;
@ -51,6 +53,7 @@ public final class Alert implements ProtectedStoragePayload, ExpirablePayload {
private final String message;
private final boolean isUpdateInfo;
private final boolean isPreReleaseInfo;
private final String version;
@Nullable
@ -68,9 +71,11 @@ public final class Alert implements ProtectedStoragePayload, ExpirablePayload {
public Alert(String message,
boolean isUpdateInfo,
boolean isPreReleaseInfo,
String version) {
this.message = message;
this.isUpdateInfo = isUpdateInfo;
this.isPreReleaseInfo = isPreReleaseInfo;
this.version = version;
}
@ -82,12 +87,14 @@ public final class Alert implements ProtectedStoragePayload, ExpirablePayload {
@SuppressWarnings("NullableProblems")
public Alert(String message,
boolean isUpdateInfo,
boolean isPreReleaseInfo,
String version,
byte[] ownerPubKeyBytes,
String signatureAsBase64,
Map<String, String> extraDataMap) {
this.message = message;
this.isUpdateInfo = isUpdateInfo;
this.isPreReleaseInfo = isPreReleaseInfo;
this.version = version;
this.ownerPubKeyBytes = ownerPubKeyBytes;
this.signatureAsBase64 = signatureAsBase64;
@ -103,6 +110,7 @@ public final class Alert implements ProtectedStoragePayload, ExpirablePayload {
protobuf.Alert.Builder builder = protobuf.Alert.newBuilder()
.setMessage(message)
.setIsUpdateInfo(isUpdateInfo)
.setIsPreReleaseInfo(isPreReleaseInfo)
.setVersion(version)
.setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes))
.setSignatureAsBase64(signatureAsBase64);
@ -119,6 +127,7 @@ public final class Alert implements ProtectedStoragePayload, ExpirablePayload {
return new Alert(proto.getMessage(),
proto.getIsUpdateInfo(),
proto.getIsPreReleaseInfo(),
proto.getVersion(),
proto.getOwnerPubKeyBytes().toByteArray(),
proto.getSignatureAsBase64(),
@ -143,7 +152,28 @@ public final class Alert implements ProtectedStoragePayload, ExpirablePayload {
ownerPubKeyBytes = Sig.getPublicKeyBytes(ownerPubKey);
}
public boolean isNewVersion() {
return Version.isNewVersion(version);
public boolean isNewVersion(Preferences preferences) {
// regular release: always notify user
// pre-release: if user has set preference to receive pre-release notification
if (isUpdateInfo ||
(isPreReleaseInfo && preferences.isNotifyOnPreRelease())) {
return Version.isNewVersion(version);
}
return false;
}
public boolean isSoftwareUpdateNotification() {
return (isUpdateInfo || isPreReleaseInfo);
}
public boolean canShowPopup(Preferences preferences) {
// only show popup if its version is newer than current
// and only if user has not checked "don't show again"
return isNewVersion(preferences) && preferences.showAgain(showAgainKey());
}
public String showAgainKey() {
return "Update_" + version;
}
}

View file

@ -33,6 +33,7 @@ import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.common.app.Version;
import bisq.common.config.Config;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import org.bitcoinj.core.Coin;
@ -73,7 +74,8 @@ public class CoreApi {
@Inject
public CoreApi(Config config,
CoreDisputeAgentsService coreDisputeAgentsService,
CoreHelpService coreHelpService, CoreOffersService coreOffersService,
CoreHelpService coreHelpService,
CoreOffersService coreOffersService,
CorePaymentAccountsService paymentAccountsService,
CorePriceService corePriceService,
CoreTradesService coreTradesService,
@ -212,8 +214,8 @@ public class CoreApi {
// Prices
///////////////////////////////////////////////////////////////////////////////////////////
public double getMarketPrice(String currencyCode) {
return corePriceService.getMarketPrice(currencyCode);
public void getMarketPrice(String currencyCode, Consumer<Double> resultHandler) {
corePriceService.getMarketPrice(currencyCode, resultHandler);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -223,12 +225,14 @@ public class CoreApi {
public void takeOffer(String offerId,
String paymentAccountId,
String takerFeeCurrencyCode,
Consumer<Trade> resultHandler) {
Consumer<Trade> resultHandler,
ErrorMessageHandler errorMessageHandler) {
Offer offer = coreOffersService.getOffer(offerId);
coreTradesService.takeOffer(offer,
paymentAccountId,
takerFeeCurrencyCode,
resultHandler);
resultHandler,
errorMessageHandler);
}
public void confirmPaymentStarted(String tradeId) {

View file

@ -54,6 +54,7 @@ import static bisq.common.util.MathUtils.scaleUpByPowerOf10;
import static bisq.core.locale.CurrencyUtil.isCryptoCurrency;
import static bisq.core.offer.OfferPayload.Direction;
import static bisq.core.offer.OfferPayload.Direction.BUY;
import static bisq.core.payment.PaymentAccountUtil.isPaymentAccountValidForOffer;
import static java.lang.String.format;
import static java.util.Comparator.comparing;
@ -64,38 +65,45 @@ class CoreOffersService {
private final Supplier<Comparator<Offer>> priceComparator = () -> comparing(Offer::getPrice);
private final Supplier<Comparator<Offer>> reversePriceComparator = () -> comparing(Offer::getPrice).reversed();
private final CoreContext coreContext;
private final KeyRing keyRing;
// Dependencies on core api services in this package must be kept to an absolute
// minimum, but some trading functions require an unlocked wallet's key, so an
// exception is made in this case.
private final CoreWalletsService coreWalletsService;
private final CreateOfferService createOfferService;
private final OfferBookService offerBookService;
private final OfferFilter offerFilter;
private final OpenOfferManager openOfferManager;
private final OfferUtil offerUtil;
private final User user;
private final boolean isApiUser;
@Inject
public CoreOffersService(CoreContext coreContext,
KeyRing keyRing,
CoreWalletsService coreWalletsService,
CreateOfferService createOfferService,
OfferBookService offerBookService,
OfferFilter offerFilter,
OpenOfferManager openOfferManager,
OfferUtil offerUtil,
User user) {
this.coreContext = coreContext;
this.keyRing = keyRing;
this.coreWalletsService = coreWalletsService;
this.createOfferService = createOfferService;
this.offerBookService = offerBookService;
this.offerFilter = offerFilter;
this.openOfferManager = openOfferManager;
this.offerUtil = offerUtil;
this.user = user;
this.isApiUser = coreContext.isApiUser();
}
Offer getOffer(String id) {
return offerBookService.getOffers().stream()
.filter(o -> o.getId().equals(id))
.filter(o -> offerFilter.canTakeOffer(o, isApiUser).isValid())
.filter(o -> !o.isMyOffer(keyRing))
.filter(o -> offerFilter.canTakeOffer(o, coreContext.isApiUser()).isValid())
.findAny().orElseThrow(() ->
new IllegalStateException(format("offer with id '%s' not found", id)));
}
@ -110,8 +118,9 @@ class CoreOffersService {
List<Offer> getOffers(String direction, String currencyCode) {
return offerBookService.getOffers().stream()
.filter(o -> !o.isMyOffer(keyRing))
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode))
.filter(o -> offerFilter.canTakeOffer(o, isApiUser).isValid())
.filter(o -> offerFilter.canTakeOffer(o, coreContext.isApiUser()).isValid())
.sorted(priceComparator(direction))
.collect(Collectors.toList());
}
@ -144,16 +153,20 @@ class CoreOffersService {
String paymentAccountId,
String makerFeeCurrencyCode,
Consumer<Offer> resultHandler) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
offerUtil.maybeSetFeePaymentCurrencyPreference(makerFeeCurrencyCode);
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
if (paymentAccount == null)
throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId));
String upperCaseCurrencyCode = currencyCode.toUpperCase();
String offerId = createOfferService.getRandomOfferId();
Direction direction = Direction.valueOf(directionAsString.toUpperCase());
Price price = Price.valueOf(upperCaseCurrencyCode, priceStringToLong(priceAsString, upperCaseCurrencyCode));
Coin amount = Coin.valueOf(amountAsLong);
Coin minAmount = Coin.valueOf(minAmountAsLong);
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
Coin useDefaultTxFee = Coin.ZERO;
Offer offer = createOfferService.createAndGetOffer(offerId,
direction,
@ -167,6 +180,8 @@ class CoreOffersService {
buyerSecurityDeposit,
paymentAccount);
verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
// We don't support atm funding from external wallet to keep it simple.
boolean useSavingsWallet = true;
//noinspection ConstantConditions
@ -203,7 +218,7 @@ class CoreOffersService {
}
void cancelOffer(String id) {
Offer offer = getOffer(id);
Offer offer = getMyOffer(id);
openOfferManager.removeOffer(offer,
() -> {
},
@ -212,6 +227,15 @@ class CoreOffersService {
});
}
private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) {
if (!isPaymentAccountValidForOffer(offer, paymentAccount)) {
String error = format("cannot create %s offer with payment account %s",
offer.getOfferPayload().getCounterCurrencyCode(),
paymentAccount.getId());
throw new IllegalStateException(error);
}
}
private void placeOffer(Offer offer,
double buyerSecurityDeposit,
long triggerPrice,

View file

@ -35,6 +35,8 @@ import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import static java.lang.String.format;
@Singleton
@Slf4j
class CorePaymentAccountsService {
@ -54,6 +56,7 @@ class CorePaymentAccountsService {
PaymentAccount createPaymentAccount(String jsonString) {
PaymentAccount paymentAccount = paymentAccountForm.toPaymentAccount(jsonString);
verifyPaymentAccountHasRequiredFields(paymentAccount);
user.addPaymentAccountIfNotExists(paymentAccount);
accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload());
log.info("Saved payment account with id {} and payment method {}.",
@ -82,4 +85,11 @@ class CorePaymentAccountsService {
File getPaymentAccountForm(String paymentMethodId) {
return paymentAccountForm.getPaymentAccountForm(paymentMethodId);
}
private void verifyPaymentAccountHasRequiredFields(PaymentAccount paymentAccount) {
// Do checks here to make sure required fields are populated.
if (paymentAccount.isTransferwiseAccount() && paymentAccount.getTradeCurrencies().isEmpty())
throw new IllegalArgumentException(format("no trade currencies defined for %s payment account",
paymentAccount.getPaymentMethod().getDisplayString().toLowerCase()));
}
}

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