mirror of
https://github.com/bisq-network/bisq.git
synced 2025-02-22 14:42:37 +01:00
Merge branch 'release/v1.6.0' into implement-segwit-for-bsq
This commit is contained in:
commit
f21379160b
379 changed files with 18082 additions and 4559 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -34,3 +34,4 @@ deploy
|
|||
/monitor/monitor-tor/*
|
||||
.java-version
|
||||
.localnet
|
||||
/apitest/src/main/resources/dao-setup*
|
||||
|
|
4
Makefile
4
Makefile
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
518
apitest/docs/api-beta-test-guide.md
Normal file
518
apitest/docs/api-beta-test-guide.md
Normal 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 Bisq’s
|
||||
pre-configured DAO / dev / regtest setup files.
|
||||
```
|
||||
$ cd api-beta-test
|
||||
$ ./gradlew clean build :apitest:installDaoSetup -x test # if you want to skip Bisq tests
|
||||
$ ./gradlew clean build :apitest:installDaoSetup # if you want to run Bisq tests
|
||||
```
|
||||
|
||||
## Running Api Test Harness
|
||||
|
||||
If your bitcoin-core binaries are in your system `PATH`, start bitcoind in regtest-mode, Bisq seednode and arbitration
|
||||
node daemons, plus Bob & Alice daemons in a bash terminal with the following bash command:
|
||||
```
|
||||
$ ./bisq-apitest --apiPassword=xyz \
|
||||
--supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon \
|
||||
--shutdownAfterTests=false
|
||||
```
|
||||
|
||||
If your bitcoin-core binaries are not in your system `PATH`, you can specify the bitcoin-core bin directory with the
|
||||
`-–bitcoinPath=<path>` option:
|
||||
```
|
||||
$ ./bisq-apitest --apiPassword=xyz \
|
||||
--supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon \
|
||||
--shutdownAfterTests=false \
|
||||
--bitcoinPath=<bitcoin-core-home>/bin
|
||||
```
|
||||
|
||||
If your bitcoin-core binaries are not statically linked to your BerkleyDB library, you can specify the path to it
|
||||
with the `–-berkeleyDbLibPath=<path>` option:
|
||||
```
|
||||
$ ./bisq-apitest --apiPassword=xyz \
|
||||
--supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon \
|
||||
--shutdownAfterTests=false \
|
||||
--bitcoinPath=<bitcoin-core-home>/bin \
|
||||
--berkeleyDbLibPath=<lib-berkleydb-path>
|
||||
```
|
||||
|
||||
Alternatively, you can specify any or all of these bisq-apitest options in a properties file located in
|
||||
`apitest/src/main/resources/apitest.properties`.
|
||||
|
||||
In this example, a beta tester uses the `apitest.properties` below, instead of `bisq-cli` options.
|
||||
```
|
||||
supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon
|
||||
apiPassword=xyz
|
||||
shutdownAfterTests=false
|
||||
bitcoinPath=/home/beta-tester/path-to-my-bitcoin-core/bin
|
||||
```
|
||||
|
||||
Start up the test harness with without command options:
|
||||
```
|
||||
$ ./bisq-apitest
|
||||
```
|
||||
|
||||
If you edit `apitest.properties`, do not forget to re-build the source. You do not need to do a full clean and
|
||||
build, or run tests. The following build command should finish quickly.
|
||||
```
|
||||
$ ./gradlew build :apitest:installDaoSetup -x test
|
||||
```
|
||||
|
||||
You should see the test harness startup bitcoin-core and other Bisq daemons in your console, run a
|
||||
`bitcoin-cli getwalletinfo` command, and generate a regtest btc block.
|
||||
|
||||
After the test harness tells you how to shut it down by entering `^C`, the test harness is ready to use.
|
||||
|
||||
## Running Trade Simulation Script
|
||||
|
||||
_Warning: again, it is assumed the beta tester has a reasonably fast machine, or the scripted wait times -- for
|
||||
the other side to perform his step in the protocol, and for btc block generation and asynchronous processing
|
||||
of new btc blocks by test daemons -- may not be long enough._
|
||||
|
||||
### System Requirements
|
||||
|
||||
Same as described at the top of this document, but your bitcoin-core’s `bitcoin-cli` binary must be in the system
|
||||
`PATH`. (The script generates regtest blocks with it.)
|
||||
|
||||
### Description
|
||||
|
||||
The regtest trade simulation script `apitest/scripts/trade-simulation.sh` is a useful introduction to the Bisq Api.
|
||||
The bash script’s output is intended to serve as a tutorial, showing how the CLI can be used to create payment
|
||||
accounts for Bob and Alice, create an offer, take the offer, and complete a trade.
|
||||
(The bash script itself is not intended to be as useful as the output.) The output is generated too quickly to
|
||||
follow in real time, so let the script complete before studying the output from start to finish.
|
||||
|
||||
The script takes four options:
|
||||
```
|
||||
-d=<direction> The trade direciton, BUY or SELL.
|
||||
-c=<country> The two letter country code, US, FR, AT, RU, etc.
|
||||
-f=<fixed-price> The offer’s fixed price.
|
||||
OR (-f and -m options mutually exclusive, use one or the other)
|
||||
-m=<margin-from-price> The offer’s margin (%) from market price.
|
||||
-a=<btc-amount> The amount of btc to buy or sell.
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
This simulation creates US / USD face-to-face payment accounts for Bob and Alice. Alice (always the trade maker)
|
||||
creates a SELL / USD offer for the amount of 0.1 BTC, at a price 2% below the current market price.
|
||||
Bob (always the taker), will use his face-to-face account to take the offer, then the two sides will complete
|
||||
the trade, checking their trade status along the way, and their BSQ / BTC balances when the trade is closed.
|
||||
```
|
||||
$ apitest/scripts/trade-simulation.sh -d sell -c us -m 2.00 -a 0.1
|
||||
```
|
||||
|
||||
In the next example, Bob and Alice create Austrian face-to-face payment accounts. Alice creates a BUY/ EUR
|
||||
offer to buy 0.125 BTC at a fixed price of 30,800 EUR.
|
||||
```
|
||||
$ apitest/scripts/trade-simulation.sh -d buy -c at -f 30800 -a 0.125
|
||||
```
|
||||
|
||||
## Manual Testing
|
||||
|
||||
The test harness used by the simulation script described in the previous section can also be used for manual CLI
|
||||
testing, and you can leave it running as you try the commands described below.
|
||||
|
||||
The Api’s default server listening port is `9998`, and you do not need to specify a `–port=<port>` option in a
|
||||
CLI command unless you change the server’s `–apiPort=<listening-port>`. In the test harness, Alice’s Api port is
|
||||
`9998`, Bob’s is `9999`. When you manually test the Api using the test harness, be aware of the port numbers being
|
||||
used in the CLI commands, so you know which server (Bob’s or Alice’s) the CLI is sending requests to.
|
||||
|
||||
### 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 CLI’s `--help` option.
|
||||
|
||||
For list of supported CLI commands:
|
||||
```
|
||||
$ ./bisq-cli --help (the –password option is not needed because there is no server request)
|
||||
```
|
||||
|
||||
For help with a specific CLI command:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --help getbalance
|
||||
OR
|
||||
$ ./bisq-cli --password=xyz getbalance --help
|
||||
```
|
||||
The position of `--help` option does not matter. If a supported positional command option is present,
|
||||
method help will be returned from the server. Also note an api password is required to get help from the server.
|
||||
|
||||
### Working With Encrypted Wallet
|
||||
|
||||
There is no need to secure your regtest Bisq wallet with an encryption password when running these examples,
|
||||
but you should encrypt your mainnet wallet as you probably already do when using the Bisq UI to transact in
|
||||
real BTC. This section explains how to encrypt your Bisq wallet with the CLI, and unlock it before performing wallet
|
||||
related operations such as creating and taking offers, checking balances, and sending BSQ and BTC to external wallets.
|
||||
|
||||
Encrypt your wallet with a password:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz setwalletpassword --wallet-password=<wallet-password>
|
||||
```
|
||||
|
||||
Set a new password on your already encrypted wallet:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz setwalletpassword --wallet-password=<wallet-password> \
|
||||
--new-wallet-password=<new-wallet-password>
|
||||
```
|
||||
|
||||
Unlock your password encrypted wallet for N seconds before performing sensitive wallet operations:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz unlockwallet --wallet-password=<wallet-password> --timeout=<seconds>
|
||||
```
|
||||
You can override a `timeout` before it expires by calling `unlockwallet` again.
|
||||
|
||||
Lock your wallet before the `unlockwallet` timeout expires:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz lockwallet
|
||||
```
|
||||
|
||||
### Checking Balances
|
||||
|
||||
Show full BSQ and BTC wallet balance information:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getbalance
|
||||
```
|
||||
|
||||
Show full BSQ wallet balance information:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9999 getbalance --currency-code=bsq
|
||||
```
|
||||
_Note: The example above is asking for Bob’s balance (using port `9999`), not Alice’s balance._
|
||||
|
||||
Show Bob’s full BTC wallet balance information:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9999 getbalance --currency-code=btc
|
||||
```
|
||||
|
||||
### Funding a Bisq Wallet
|
||||
|
||||
#### Receiving BTC
|
||||
|
||||
To receive BTC from an external wallet, find an unused BTC address (with a zero balance) to receive the BTC.
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getfundingaddresses
|
||||
```
|
||||
You can check a block explorer for the status of a transaction, or you can check your Bisq BTC wallet address directly:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getaddressbalance --address=<btc-address>
|
||||
```
|
||||
|
||||
#### Receiving BSQ
|
||||
To receive BSQ from an external wallet, find an unused BSQ address:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getunusedbsqaddress
|
||||
```
|
||||
|
||||
Give the public address to the sender. After the BSQ is sent, you can check block explorers for the status of
|
||||
the transaction. There is no support (yet) to check the balance of an individual BSQ address in your wallet,
|
||||
but you can check your BSQ wallet’s balance to determine if the new funds have arrived:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9999 getbalance --currency-code=bsq
|
||||
```
|
||||
|
||||
### Sending BSQ and BTC to External Wallets
|
||||
|
||||
Below are commands for sending BSQ and BTC to external wallets.
|
||||
|
||||
Send BSQ:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 sendbsq --address=<bsq-address> --amount=<bsq-amount>
|
||||
```
|
||||
_Note: Sending BSQ to non-Bisq wallets is not supported and highly discouraged._
|
||||
|
||||
Send BSQ with a withdrawal transaction fee of 10 sats/byte:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 sendbsq --address=<bsq-address> --amount=<bsq-amount> --tx-fee-rate=10
|
||||
```
|
||||
|
||||
Send BTC:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 sendbtc --address=<btc-address> --amount=<btc-amount>
|
||||
```
|
||||
Send BTC with a withdrawal transaction fee of 20 sats/byte:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 sendbtc --address=<btc-address> --amount=<btc-amount> --tx-fee-rate=20
|
||||
```
|
||||
|
||||
### Withdrawal Transaction Fees
|
||||
|
||||
If you have traded using the Bisq UI, you are probably aware of the default network bitcoin withdrawal transaction
|
||||
fee and custom withdrawal transaction fee user preference in the UI’s setting view. The Api uses these same
|
||||
withdrawal transaction fee rates, and affords a third – as mentioned in the previous section -- withdrawal
|
||||
transaction fee option in the `sendbsq` and `sendbtc` commands. The `sendbsq` and `sendbtc` commands'
|
||||
`--tx-fee-rate=<sats/byte>` options override both the default network fee rate, and your custom transaction fee
|
||||
setting for the execution of those commands.
|
||||
|
||||
#### Using Default Network Transaction Fee
|
||||
|
||||
If you have not set your custom withdrawal transaction fee setting, the default network transaction fee will be used
|
||||
when withdrawing funds. In either case, you can check the current (default or custom) withdrawal transaction fee rate:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz gettxfeerate
|
||||
```
|
||||
#### Setting Custom Transaction Fee Preference
|
||||
To set a custom withdrawal transaction fee rate preference of 50 sats/byte:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz settxfeerate --tx-fee-rate=50
|
||||
```
|
||||
|
||||
#### Removing User’s Custom Transaction Fee Preference
|
||||
To remove a custom withdrawal transaction fee rate preference, and revert to the network fee rate:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz unsettxfeerate
|
||||
```
|
||||
|
||||
### Creating Test Payment Accounts
|
||||
|
||||
Creating a payment account using the Api involves three steps:
|
||||
|
||||
1. Find the payment-method-id for the payment account type you wish to create. For example, if you want to
|
||||
create a face-to-face type payment account, find the face-to-face payment-method-id (`F2F`):
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getpaymentmethods
|
||||
```
|
||||
|
||||
2. Use the payment-method-id `F2F` found in the `getpaymentmethods` command output to create a blank payment account
|
||||
form:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getpaymentacctform --payment-method-id=F2F
|
||||
```
|
||||
This `getpaymentacctform` command generates a json file (form) for creating an `F2F` payment account,
|
||||
prints the file’s contents, and tells you where it is. In this example, the sever created an `F2F` account
|
||||
form named `f2f_1612381824625.json`.
|
||||
|
||||
|
||||
3. Manually edit the json file, and use its path in the `createpaymentacct` command.
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 createpaymentacct \
|
||||
--payment-account-form=f2f_1612381824625.json
|
||||
```
|
||||
_Note: You can rename the file before passing it to the `createpaymentacct` command._
|
||||
|
||||
The server will create and save the new payment account from details defined in the json file then
|
||||
return the new payment account to the CLI. The CLI will display the account ID with other details
|
||||
in the console, but if you ever need to find a payment account ID, use the `getpaymentaccts` command:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getpaymentaccts
|
||||
```
|
||||
|
||||
### Creating Offers
|
||||
|
||||
The createoffer command is the Api's most complex command (so far), but CLI posix-style options are self-explanatory,
|
||||
and CLI `createoffer` command help gives you specific information about each option.
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 createoffer --help
|
||||
```
|
||||
|
||||
#### Examples
|
||||
|
||||
The `trade-simulation.sh` script described above is an easy way to figure out how to use this command.
|
||||
In a previous example, Alice created a BUY/ EUR offer to buy 0.125 BTC at a fixed price of 30,800 EUR,
|
||||
and pay the Bisq maker fee in BSQ. Alice had already created an EUR face-to-face payment account with id
|
||||
`f3c1ec8b-9761-458d-b13d-9039c6892413`, and used this `createoffer` command:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 createoffer \
|
||||
--payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \
|
||||
--direction=BUY \
|
||||
--currency-code=EUR \
|
||||
--amount=0.125 \
|
||||
--fixed-price=30800 \
|
||||
--security-deposit=15.0 \
|
||||
--fee-currency=BSQ
|
||||
```
|
||||
|
||||
If Alice was in Japan, and wanted to create an offer to sell 0.125 BTC at 0.5% above the current market JPY price,
|
||||
putting up a 15% security deposit, the `createoffer` command to do that would be:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 createoffer \
|
||||
--payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \
|
||||
--direction=SELL \
|
||||
--currency-code=JPY \
|
||||
--amount=0.125 \
|
||||
--market-price-margin=0.5 \
|
||||
--security-deposit=15.0 \
|
||||
--fee-currency=BSQ
|
||||
```
|
||||
|
||||
The `trade-simulation.sh` script options that would generate the previous `createoffer` example is:
|
||||
```
|
||||
$ apitest/scripts/trade-simulation.sh -d sell -c jp -m 0.5 -a 0.125
|
||||
```
|
||||
|
||||
### Browsing Your Own Offers
|
||||
|
||||
There are different commands to browse available offers you can take, and offers you created.
|
||||
|
||||
To see all offers you created with a specific direction (BUY|SELL) and currency (CAD|EUR|USD|...):
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getmyoffers --direction=<BUY|SELL> --currency-code=<currency-code>
|
||||
```
|
||||
|
||||
To look at a specific offer you created:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getmyoffer --offer-id=<offer-id>
|
||||
```
|
||||
|
||||
### Browsing Available Offers
|
||||
|
||||
To see all available offers you can take, with a specific direction (BUY|SELL) and currency (CAD|EUR|USD|...):
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getoffers --direction=<BUY|SELL> --currency-code=<currency-code>
|
||||
```
|
||||
|
||||
To look at a specific, available offer you could take:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getoffer --offer-id=<offer-id>
|
||||
```
|
||||
|
||||
### Removing An Offer
|
||||
|
||||
To cancel one of your offers:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 canceloffer --offer-id=<offer-id>
|
||||
```
|
||||
The offer will be removed from other Bisq users' offer views, and paid transaction fees will be forfeited.
|
||||
|
||||
### Editing an Existing Offer
|
||||
|
||||
Editing existing offers is not yet supported. You can cancel and re-create an offer, but paid transaction fees
|
||||
for the canceled offer will be forfeited.
|
||||
|
||||
### Taking Offers
|
||||
|
||||
Taking an available offer involves two CLI commands: `getoffers` and `takeoffer`.
|
||||
|
||||
A CLI user browses available offers with the getoffers command. For example, the user browses SELL / EUR offers:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 getoffers --direction=SELL --currency-code=EUR
|
||||
```
|
||||
|
||||
And takes one of the available offers with an EUR payment account ( id `fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e`)
|
||||
with the `takeoffer` command:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 takeoffer \
|
||||
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
|
||||
--payment-account=fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e \
|
||||
--fee-currency=btc
|
||||
```
|
||||
The taken offer will be used to create a trade contract. The next section describes how to use the Api to execute
|
||||
the trade.
|
||||
|
||||
### Completing Trade Protocol
|
||||
|
||||
The first step in the Bisq trade protocol is completed when a `takeoffer` command successfully creates a new trade from
|
||||
the taken offer. After the Bisq nodes prepare the trade, its status can be viewed with the `gettrade` command:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 gettrade --trade-id=<trade-id>
|
||||
```
|
||||
The `trade-id` is the same as the taken `offer-id`, but when viewing and interacting with trades, it is referred to as
|
||||
the `trade-id`. Note that the `trade-id` argument is a full `offer-id`, not a truncated `short-id` as displayed in the
|
||||
Bisq UI.
|
||||
|
||||
You can also view the entire trade contract in `json` format by using the `gettrade` command's `--show-contract=true`
|
||||
option:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 gettrade --trade-id=<trade-id> --show-contract=true
|
||||
```
|
||||
|
||||
|
||||
The `gettrade` command’s output shows the state of the trade from initial preparation through completion and closure.
|
||||
Output columns include:
|
||||
```
|
||||
Deposit Published YES if the taker fee tx deposit has been broadcast to the network.
|
||||
Deposit Confirmed YES if the taker fee tx deposit has been confirmed by the network.
|
||||
Fiat Sent YES if the buyer has sent a “payment started” message to seller.
|
||||
Fiat Received YES if the seller has sent a “payment received” message to buyer.
|
||||
Payout Published YES if the seller’s BTC payout tx has been broadcast to the network.
|
||||
Withdrawn YES if the buyer’s BTC proceeds have been sent to an external wallet.
|
||||
```
|
||||
Trade status information informs both sides of a trade which steps have been completed, and which step to perform next.
|
||||
It should be frequently checked by both sides before proceeding to the next step of the protocol.
|
||||
|
||||
_Note: There is some delay after a new trade is created due to the time it takes for a taker’s trade deposit fee
|
||||
transaction to be published and confirmed on the bitcoin network. Both sides of the trade can check the `gettrade`
|
||||
output's `Deposit Published` and `Deposit Confirmed` columns to find out when this early phase of the trade protocol is
|
||||
complete._
|
||||
|
||||
Once the taker fee transaction has been confirmed, payment can be sent, payment receipt confirmed, and the trade
|
||||
protocol completed. There are three CLI commands that must be performed in coordinated order by each side of the trade:
|
||||
```
|
||||
confirmpaymentstarted Buyer sends seller a message confirming payment has been sent.
|
||||
confirmpaymentreceived Seller sends buyer a message confirming payment has been received.
|
||||
keepfunds Keep trade proceeds in their Bisq wallets.
|
||||
OR
|
||||
withdrawfunds Send trade proceeds to an external wallet.
|
||||
```
|
||||
The last two mutually exclusive commands (`keepfunds` or `withdrawfunds`) may seem unnecessary, but they are critical
|
||||
because they inform the Bisq node that a trade’s state can be set to `CLOSED`. Please close out your trades with one
|
||||
or the other command.
|
||||
|
||||
Each of the CLI commands above takes one argument: `--trade-id=<trade-id>`:
|
||||
```
|
||||
$ ./bisq-cli --password=xyz --port=9998 confirmpaymentstarted --trade-id=<trade-id>
|
||||
$ ./bisq-cli --password=xyz --port=9999 confirmpaymentreceived --trade-id=<trade-id>
|
||||
$ ./bisq-cli --password=xyz --port=9998 keepfunds --trade-id=<trade-id>
|
||||
$ ./bisq-cli --password=xyz --port=9999 withdrawfunds --trade-id=<trade-id> --address=<btc-address> [--memo=<"memo">]
|
||||
```
|
||||
|
||||
## Shutting Down Test Harness
|
||||
|
||||
The test harness should cleanly shutdown all the background apps in proper order after entering ^C.
|
||||
|
||||
Once shutdown, all Bisq and bitcoin-core data files are left in the state they were in at shutdown time,
|
||||
so they and logs can be examined after a test run. All datafiles will be refreshed the next time the test harness
|
||||
is started, so if you want to save datafiles and logs from a test run, copy them to a safe place first.
|
||||
They can be found in `apitest/build/resources/main`.
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
28
apitest/scripts/editf2faccountform.py
Normal file
28
apitest/scripts/editf2faccountform.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
import sys, os, json
|
||||
|
||||
# Writes a Bisq json F2F payment account form for the given country_code to the current working directory.
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: editf2faccountform.py country_code")
|
||||
exit(1)
|
||||
|
||||
country_code = str(sys.argv[1]).upper()
|
||||
acct_form = {
|
||||
"_COMMENTS_": [
|
||||
"Do not manually edit the paymentMethodId field.",
|
||||
"Edit the salt field only if you are recreating a payment account on a new installation and wish to preserve the account age."
|
||||
],
|
||||
"paymentMethodId": "F2F",
|
||||
"accountName": "Face to Face Payment Account",
|
||||
"city": "Anytown",
|
||||
"contact": "Me",
|
||||
"country": country_code,
|
||||
"extraInfo": "",
|
||||
"salt": ""
|
||||
}
|
||||
target=os.path.dirname(os.path.realpath(__file__)) + '/' + 'f2f-acct.json'
|
||||
with open (target, 'w') as outfile:
|
||||
json.dump(acct_form, outfile, indent=2)
|
||||
outfile.write('\n')
|
||||
|
||||
exit(0)
|
159
apitest/scripts/limit-order-simulation.sh
Executable file
159
apitest/scripts/limit-order-simulation.sh
Executable file
|
@ -0,0 +1,159 @@
|
|||
#! /bin/bash
|
||||
|
||||
# Demonstrates a way to create a limit order (offer) using the API CLI with a local regtest bitcoin node.
|
||||
#
|
||||
# A country code argument is used to create a country based face to face payment account for the simulated offer.
|
||||
#
|
||||
# Prerequisites:
|
||||
#
|
||||
# - Linux or OSX with bash, Java 10, or Java 11-12 (JDK language compatibility 10), and bitcoin-core (v0.19, v0.20).
|
||||
#
|
||||
# - 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
|
|
@ -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" ]
|
||||
}
|
||||
|
|
119
apitest/scripts/rolling-offer-simulation.sh
Executable file
119
apitest/scripts/rolling-offer-simulation.sh
Executable file
|
@ -0,0 +1,119 @@
|
|||
#! /bin/bash
|
||||
|
||||
# Demonstrates a way to always keep one offer in the market, using the API CLI with a local regtest bitcoin node.
|
||||
# Alice creates an offer, waits for Bob to take it, and completes the trade protocol with him. Then Alice
|
||||
# creates a new offer...
|
||||
#
|
||||
# Stop the script by entering ^C.
|
||||
#
|
||||
# A country code argument is used to create a country based face to face payment account for the simulated offer.
|
||||
#
|
||||
# Prerequisites:
|
||||
#
|
||||
# - Linux or OSX with bash, Java 10, or Java 11-12 (JDK language compatibility 10), and bitcoin-core (v0.19, v0.20).
|
||||
#
|
||||
# - Bisq must be fully built with apitest dao setup files installed.
|
||||
# Build command: `./gradlew clean build :apitest:installDaoSetup`
|
||||
#
|
||||
# - All supporting nodes must be run locally, in dev/dao/regtest mode:
|
||||
# bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon
|
||||
#
|
||||
# These should be run using the apitest harness. From the root project dir, run:
|
||||
# `$ ./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false`
|
||||
#
|
||||
# - Only regtest btc can be bought or sold with the test payment account.
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# This script must be run from the root of the project, e.g.:
|
||||
#
|
||||
# `$ apitest/scripts/rolling-offer-simulation.sh -d buy -c us -m 2.00 -a 0.125`
|
||||
#
|
||||
# Script options: -d <direction> -c <country-code> (-m <mkt-price-margin(%)> || -f <fixed-price>) -a <amount(btc)>
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# Create a buy/usd offer to sell 0.1 btc at 2% above market price, using a US face to face payment account:
|
||||
#
|
||||
# `$ apitest/scripts/rolling-offer-simulation.sh -d sell -c us -m 2.00 -a 0.1`
|
||||
|
||||
|
||||
APP_BASE_NAME=$(basename "$0")
|
||||
APP_HOME=$(pwd -P)
|
||||
APITEST_SCRIPTS_HOME="$APP_HOME/apitest/scripts"
|
||||
|
||||
source "$APITEST_SCRIPTS_HOME/trade-simulation-env.sh"
|
||||
source "$APITEST_SCRIPTS_HOME/trade-simulation-utils.sh"
|
||||
|
||||
checksetup
|
||||
parseopts "$@"
|
||||
|
||||
printdate "Started $APP_BASE_NAME with parameters:"
|
||||
printscriptparams
|
||||
printbreak
|
||||
|
||||
registerdisputeagents
|
||||
|
||||
showcreatepaymentacctsteps "Alice" "$ALICE_PORT"
|
||||
|
||||
CMD="$CLI_BASE --port=$ALICE_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM"
|
||||
printdate "ALICE CLI: $CMD"
|
||||
CMD_OUTPUT=$(createpaymentacct "$CMD")
|
||||
echo "$CMD_OUTPUT"
|
||||
printbreak
|
||||
export ALICE_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT")
|
||||
export CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT")
|
||||
printdate "Alice's F2F payment-account-id: $ALICE_ACCT_ID, currency-code: $CURRENCY_CODE"
|
||||
exitoncommandalert $?
|
||||
printbreak
|
||||
|
||||
printdate "Bob creates his F2F payment account."
|
||||
CMD="$CLI_BASE --port=$BOB_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM"
|
||||
printdate "BOB CLI: $CMD"
|
||||
CMD_OUTPUT=$(createpaymentacct "$CMD")
|
||||
echo "$CMD_OUTPUT"
|
||||
printbreak
|
||||
export BOB_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT")
|
||||
export CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT")
|
||||
printdate "Bob's F2F payment-account-id: $BOB_ACCT_ID, currency-code: $CURRENCY_CODE"
|
||||
exitoncommandalert $?
|
||||
printbreak
|
||||
|
||||
while : ; do
|
||||
printdate "ALICE $ALICE_ROLE: Creating $DIRECTION $CURRENCY_CODE offer with payment acct $ALICE_ACCT_ID."
|
||||
CURRENT_PRICE=$(getcurrentprice "$ALICE_PORT" "$CURRENCY_CODE")
|
||||
exitoncommandalert $?
|
||||
printdate "Current Market Price: $CURRENT_PRICE"
|
||||
CMD=$(gencreateoffercommand "$ALICE_PORT" "$ALICE_ACCT_ID")
|
||||
printdate "ALICE CLI: $CMD"
|
||||
OFFER_ID=$(createoffer "$CMD")
|
||||
exitoncommandalert $?
|
||||
printdate "ALICE $ALICE_ROLE: Created offer with id: $OFFER_ID."
|
||||
printbreak
|
||||
sleeptraced 3
|
||||
|
||||
# Show Alice's new offer.
|
||||
printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer."
|
||||
CMD="$CLI_BASE --port=$ALICE_PORT getmyoffer --offer-id=$OFFER_ID"
|
||||
printdate "ALICE CLI: $CMD"
|
||||
OFFER=$($CMD)
|
||||
exitoncommandalert $?
|
||||
echo "$OFFER"
|
||||
printbreak
|
||||
sleeptraced 3
|
||||
|
||||
# Generate some btc blocks.
|
||||
printdate "Generating btc blocks after publishing Alice's offer."
|
||||
genbtcblocks 3 2
|
||||
printbreak
|
||||
|
||||
RANDOM_WAIT=$(echo $[$RANDOM % 10 + 1])
|
||||
printdate "Bob will take Alice's offer in $RANDOM_WAIT seconds..."
|
||||
sleeptraced "$RANDOM_WAIT"
|
||||
|
||||
executetrade
|
||||
exitoncommandalert $?
|
||||
printbreak
|
||||
done
|
||||
|
||||
exit 0
|
312
apitest/scripts/trade-simulation-env.sh
Executable file
312
apitest/scripts/trade-simulation-env.sh
Executable file
|
@ -0,0 +1,312 @@
|
|||
#! /bin/bash
|
||||
|
||||
# This file must be sourced by the main driver.
|
||||
|
||||
export CLI_BASE="./bisq-cli --password=xyz"
|
||||
export ARBITRATOR_PORT=9997
|
||||
export ALICE_PORT=9998
|
||||
export BOB_PORT=9999
|
||||
export F2F_ACCT_FORM="f2f-acct.json"
|
||||
|
||||
checkos() {
|
||||
LINUX=FALSE
|
||||
DARWIN=FALSE
|
||||
UNAME=$(uname)
|
||||
case "$UNAME" in
|
||||
Linux* )
|
||||
export LINUX=TRUE
|
||||
;;
|
||||
Darwin* )
|
||||
export DARWIN=TRUE
|
||||
;;
|
||||
esac
|
||||
if [[ "$LINUX" == "TRUE" ]]; then
|
||||
printdate "Running on supported Linux OS."
|
||||
elif [[ "$DARWIN" == "TRUE" ]]; then
|
||||
printdate "Running on supported Mac OS."
|
||||
else
|
||||
printdate "Script cannot run on $OSTYPE OS, only Linux and OSX are supported."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
checksetup() {
|
||||
checkos
|
||||
|
||||
apitestusage() {
|
||||
echo "The apitest harness must be running a local bitcoin regtest node, a seednode, an arbitration node,"
|
||||
echo "Bob & Alice daemons, and bitcoin-core's bitcoin-cli must be in the system PATH."
|
||||
echo ""
|
||||
echo "From the project's root dir, start all supporting nodes from a terminal:"
|
||||
echo "./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false"
|
||||
exit 1;
|
||||
}
|
||||
printdate "Checking $APP_HOME for some expected directories and files."
|
||||
if [ -d "$APP_HOME/apitest" ]; then
|
||||
printdate "Subproject apitest exists.";
|
||||
else
|
||||
printdate "Error: Subproject apitest not found, maybe because you are not running the script from the project root dir."
|
||||
exit 1
|
||||
fi
|
||||
if [ -f "$APP_HOME/bisq-cli" ]; then
|
||||
printdate "The bisq-cli script exists.";
|
||||
else
|
||||
printdate "Error: The bisq-cli script not found, maybe because you are not running the script from the project root dir."
|
||||
exit 1
|
||||
fi
|
||||
printdate "Checking to see local bitcoind is running, and bitcoin-cli is in PATH."
|
||||
checkbitcoindrunning
|
||||
checkbitcoincliinpath
|
||||
printdate "Checking to see bisq servers are running."
|
||||
checkseednoderunning
|
||||
checkarbnoderunning
|
||||
checkalicenoderunning
|
||||
checkbobnoderunning
|
||||
}
|
||||
|
||||
parseopts() {
|
||||
usage() {
|
||||
echo "Usage: $0 [-d buy|sell] [-c <country-code>] [-f <fixed-price> || -m <margin-from-price>] [-a <amount in btc>]" 1>&2
|
||||
exit 1;
|
||||
}
|
||||
|
||||
local OPTIND o d c f m a
|
||||
while getopts "d:c:f:m:a:" o; do
|
||||
case "${o}" in
|
||||
d) d=$(echo "${OPTARG}" | tr '[:lower:]' '[:upper:]')
|
||||
((d == "BUY" || d == "SELL")) || usage
|
||||
export DIRECTION=${d}
|
||||
;;
|
||||
c) c=$(echo "${OPTARG}"| tr '[:lower:]' '[:upper:]')
|
||||
export COUNTRY_CODE=${c}
|
||||
;;
|
||||
f) f=${OPTARG}
|
||||
export FIXED_PRICE=${f}
|
||||
;;
|
||||
m) m=${OPTARG}
|
||||
export MKT_PRICE_MARGIN=${m}
|
||||
;;
|
||||
a) a=${OPTARG}
|
||||
export AMOUNT=${a}
|
||||
;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND-1))
|
||||
|
||||
if [ -z "${d}" ] || [ -z "${c}" ] || [ -z "${a}" ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
if [ -z "${f}" ] && [ -z "${m}" ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
if [ -n "${f}" ] && [ -n "${m}" ]; then
|
||||
printdate "Must use margin-from-price param (-m) or fixed-price param (-f), not both."
|
||||
usage
|
||||
fi
|
||||
|
||||
if [ "$DIRECTION" = "SELL" ]
|
||||
then
|
||||
export BOB_ROLE="(taker/buyer)"
|
||||
export ALICE_ROLE="(maker/seller)"
|
||||
else
|
||||
export BOB_ROLE="(taker/seller)"
|
||||
export ALICE_ROLE="(maker/buyer)"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
parselimitorderopts() {
|
||||
usage() {
|
||||
echo "Usage: $0 [-l limit-price] [-d buy|sell] [-c <country-code>] [-f <fixed-price> || -m <margin-from-price>] [-a <amount in btc>] [-w <price-poll-interval(s)>]" 1>&2
|
||||
exit 1;
|
||||
}
|
||||
|
||||
local OPTIND o l d c f m a w
|
||||
while getopts "l:d:c:f:m:a:w:" o; do
|
||||
case "${o}" in
|
||||
l) l=${OPTARG}
|
||||
export LIMIT_PRICE=${l}
|
||||
;;
|
||||
d) d=$(echo "${OPTARG}" | tr '[:lower:]' '[:upper:]')
|
||||
((d == "BUY" || d == "SELL")) || usage
|
||||
export DIRECTION=${d}
|
||||
;;
|
||||
c) c=$(echo "${OPTARG}"| tr '[:lower:]' '[:upper:]')
|
||||
export COUNTRY_CODE=${c}
|
||||
;;
|
||||
f) f=${OPTARG}
|
||||
export FIXED_PRICE=${f}
|
||||
;;
|
||||
m) m=${OPTARG}
|
||||
export MKT_PRICE_MARGIN=${m}
|
||||
;;
|
||||
a) a=${OPTARG}
|
||||
export AMOUNT=${a}
|
||||
;;
|
||||
w) w=${OPTARG}
|
||||
export WAIT=${w}
|
||||
;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND-1))
|
||||
|
||||
if [ -z "${l}" ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
if [ -z "${d}" ] || [ -z "${c}" ] || [ -z "${a}" ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
if [ -z "${f}" ] && [ -z "${m}" ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
if [ -n "${f}" ] && [ -n "${m}" ]; then
|
||||
printdate "Must use margin-from-price param (-m) or fixed-price param (-f), not both."
|
||||
usage
|
||||
fi
|
||||
|
||||
if [ -z "${w}" ]; then
|
||||
WAIT=120
|
||||
elif [ "$w" -lt 20 ]; then
|
||||
printdate "The -w <price-poll-interval(s)> option is too low, minimum allowed is 20s. Using default 120s."
|
||||
WAIT=120
|
||||
fi
|
||||
}
|
||||
|
||||
checkbitcoindrunning() {
|
||||
# There may be a '+' char in the path and we have to escape it for pgrep.
|
||||
if [[ $APP_HOME == *"+"* ]]; then
|
||||
ESCAPED_APP_HOME=$(escapepluschar "$APP_HOME")
|
||||
else
|
||||
ESCAPED_APP_HOME="$APP_HOME"
|
||||
fi
|
||||
if pgrep -f "bitcoind -datadir=$ESCAPED_APP_HOME/apitest/build/resources/main/Bitcoin-regtest" > /dev/null ; then
|
||||
printdate "The regtest bitcoind node is running on host."
|
||||
else
|
||||
printdate "Error: regtest bitcoind node is not running on host, exiting."
|
||||
apitestusage
|
||||
fi
|
||||
}
|
||||
|
||||
checkbitcoincliinpath() {
|
||||
if which bitcoin-cli > /dev/null ; then
|
||||
printdate "The bitcoin-cli binary is in the system PATH."
|
||||
else
|
||||
printdate "Error: bitcoin-cli binary is not in the system PATH, exiting."
|
||||
apitestusage
|
||||
fi
|
||||
}
|
||||
|
||||
checkseednoderunning() {
|
||||
if [[ "$LINUX" == "TRUE" ]]; then
|
||||
if pgrep -f "bisq.seednode.SeedNodeMain" > /dev/null ; then
|
||||
printdate "The seed node is running on host."
|
||||
else
|
||||
printdate "Error: seed node is not running on host, exiting."
|
||||
apitestusage
|
||||
fi
|
||||
elif [[ "$DARWIN" == "TRUE" ]]; then
|
||||
if ps -A | awk '/[S]eedNodeMain/ {print $1}' > /dev/null ; then
|
||||
printdate "The seednode is running on host."
|
||||
else
|
||||
printdate "Error: seed node is not running on host, exiting."
|
||||
apitestusage
|
||||
fi
|
||||
else
|
||||
printdate "Error: seed node is not running on host, exiting."
|
||||
apitestusage
|
||||
fi
|
||||
}
|
||||
|
||||
checkarbnoderunning() {
|
||||
if [[ "$LINUX" == "TRUE" ]]; then
|
||||
if pgrep -f "bisq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Arb_dao" > /dev/null ; then
|
||||
printdate "The arbitration node is running on host."
|
||||
else
|
||||
printdate "Error: arbitration node is not running on host, exiting."
|
||||
apitestusage
|
||||
fi
|
||||
elif [[ "$DARWIN" == "TRUE" ]]; then
|
||||
if ps -A | awk '/[b]isq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Arb_dao/ {print $1}' > /dev/null ; then
|
||||
printdate "The arbitration node is running on host."
|
||||
else
|
||||
printdate "Error: arbitration node is not running on host, exiting."
|
||||
apitestusage
|
||||
fi
|
||||
else
|
||||
printdate "Error: arbitration node is not running on host, exiting."
|
||||
apitestusage
|
||||
fi
|
||||
}
|
||||
|
||||
checkalicenoderunning() {
|
||||
if [[ "$LINUX" == "TRUE" ]]; then
|
||||
if pgrep -f "bisq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Alice_dao" > /dev/null ; then
|
||||
printdate "Alice's node is running on host."
|
||||
else
|
||||
printdate "Error: Alice's node is not running on host, exiting."
|
||||
apitestusage
|
||||
fi
|
||||
elif [[ "$DARWIN" == "TRUE" ]]; then
|
||||
if ps -A | awk '/[b]isq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Alice_dao/ {print $1}' > /dev/null ; then
|
||||
printdate "Alice's node node is running on host."
|
||||
else
|
||||
printdate "Error: Alice's node is not running on host, exiting."
|
||||
apitestusage
|
||||
fi
|
||||
else
|
||||
printdate "Error: Alice's node is not running on host, exiting."
|
||||
apitestusage
|
||||
fi
|
||||
}
|
||||
|
||||
checkbobnoderunning() {
|
||||
if [[ "$LINUX" == "TRUE" ]]; then
|
||||
if pgrep -f "bisq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Alice_dao" > /dev/null ; then
|
||||
printdate "Bob's node is running on host."
|
||||
else
|
||||
printdate "Error: Bob's node is not running on host, exiting."
|
||||
apitestusage
|
||||
fi
|
||||
elif [[ "$DARWIN" == "TRUE" ]]; then
|
||||
if ps -A | awk '/[b]isq.daemon.app.BisqDaemonMain --appName=bisq-BTC_REGTEST_Alice_dao/ {print $1}' > /dev/null ; then
|
||||
printdate "Bob's node node is running on host."
|
||||
else
|
||||
printdate "Error: Bob's node is not running on host, exiting."
|
||||
apitestusage
|
||||
fi
|
||||
else
|
||||
printdate "Error: Bob's node is not running on host, exiting."
|
||||
apitestusage
|
||||
fi
|
||||
}
|
||||
|
||||
printscriptparams() {
|
||||
if [ -n "${LIMIT_PRICE+1}" ]; then
|
||||
echo " LIMIT_PRICE = $LIMIT_PRICE"
|
||||
fi
|
||||
|
||||
echo " DIRECTION = $DIRECTION"
|
||||
echo " COUNTRY_CODE = $COUNTRY_CODE"
|
||||
echo " FIXED_PRICE = $FIXED_PRICE"
|
||||
echo " MKT_PRICE_MARGIN = $MKT_PRICE_MARGIN"
|
||||
echo " AMOUNT = $AMOUNT"
|
||||
|
||||
if [ -n "${BOB_ROLE+1}" ]; then
|
||||
echo " BOB_ROLE = $BOB_ROLE"
|
||||
fi
|
||||
|
||||
if [ -n "${ALICE_ROLE+1}" ]; then
|
||||
echo " ALICE_ROLE = $ALICE_ROLE"
|
||||
fi
|
||||
|
||||
if [ -n "${WAIT+1}" ]; then
|
||||
echo " WAIT = $WAIT"
|
||||
fi
|
||||
}
|
603
apitest/scripts/trade-simulation-utils.sh
Executable file
603
apitest/scripts/trade-simulation-utils.sh
Executable 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"
|
||||
}
|
126
apitest/scripts/trade-simulation.sh
Executable file
126
apitest/scripts/trade-simulation.sh
Executable file
|
@ -0,0 +1,126 @@
|
|||
#! /bin/bash
|
||||
|
||||
# Runs fiat <-> btc trading scenarios using the API CLI with a local regtest bitcoin node.
|
||||
#
|
||||
# A country code argument is used to create a country based face to face payment account for the simulated
|
||||
# trade, and the maker's face to face payment account's currency code is used when creating the offer.
|
||||
#
|
||||
# Prerequisites:
|
||||
#
|
||||
# - Linux or OSX with bash, Java 10, or Java 11-12 (JDK language compatibility 10), and bitcoin-core (v0.19, v0.20).
|
||||
#
|
||||
# - 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
|
|
@ -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()
|
||||
|
|
|
@ -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";
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,6 +42,8 @@ public class OfferTest extends AbstractOfferTest {
|
|||
public void testAmtTooLargeShouldThrowException() {
|
||||
ValidateCreateOfferTest test = new ValidateCreateOfferTest();
|
||||
test.testAmtTooLargeShouldThrowException();
|
||||
test.testNoMatchingEURPaymentAccountShouldThrowException();
|
||||
test.testNoMatchingCADPaymentAccountShouldThrowException();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -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
|
||||
|
|
121
apitest/src/test/java/bisq/apitest/scenario/ScriptedBotTest.java
Normal file
121
apitest/src/test/java/bisq/apitest/scenario/ScriptedBotTest.java
Normal file
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.apitest.scenario;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
|
||||
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
|
||||
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
|
||||
import static bisq.apitest.config.BisqAppConfig.arbdaemon;
|
||||
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
|
||||
import static bisq.apitest.config.BisqAppConfig.seednode;
|
||||
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.startShutdownTimer;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
||||
|
||||
import bisq.apitest.config.ApiTestConfig;
|
||||
import bisq.apitest.method.BitcoinCliHelper;
|
||||
import bisq.apitest.scenario.bot.AbstractBotTest;
|
||||
import bisq.apitest.scenario.bot.BotClient;
|
||||
import bisq.apitest.scenario.bot.RobotBob;
|
||||
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
|
||||
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
|
||||
|
||||
// The test case is enabled if AbstractBotTest#botScriptExists() returns true.
|
||||
@EnabledIf("botScriptExists")
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class ScriptedBotTest extends AbstractBotTest {
|
||||
|
||||
private RobotBob robotBob;
|
||||
|
||||
@BeforeAll
|
||||
public static void startTestHarness() {
|
||||
botScript = deserializeBotScript();
|
||||
|
||||
if (botScript.isUseTestHarness()) {
|
||||
startSupportingApps(true,
|
||||
true,
|
||||
bitcoind,
|
||||
seednode,
|
||||
arbdaemon,
|
||||
alicedaemon,
|
||||
bobdaemon);
|
||||
} else {
|
||||
// We need just enough configurations to make sure Bob and testers use
|
||||
// the right apiPassword, to create a bitcoin-cli helper, and RobotBob's
|
||||
// gRPC stubs. But the user will have to register dispute agents before
|
||||
// an offer can be taken.
|
||||
config = new ApiTestConfig("--apiPassword", "xyz");
|
||||
bitcoinCli = new BitcoinCliHelper(config);
|
||||
log.warn("Don't forget to register dispute agents before trying to trade with me.");
|
||||
}
|
||||
|
||||
botClient = new BotClient(bobClient);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void initRobotBob() {
|
||||
try {
|
||||
BashScriptGenerator bashScriptGenerator = getBashScriptGenerator();
|
||||
robotBob = new RobotBob(botClient, botScript, bitcoinCli, bashScriptGenerator);
|
||||
} catch (Exception ex) {
|
||||
fail(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
public void runRobotBob() {
|
||||
try {
|
||||
|
||||
startShutdownTimer();
|
||||
robotBob.run();
|
||||
|
||||
} catch (ManualBotShutdownException ex) {
|
||||
// This exception is thrown if a /tmp/bottest-shutdown file was found.
|
||||
// You can also kill -15 <pid>
|
||||
// of worker.org.gradle.process.internal.worker.GradleWorkerMain 'Gradle Test Executor #'
|
||||
//
|
||||
// This will cleanly shut everything down as well, but you will see a
|
||||
// Process 'Gradle Test Executor #' finished with non-zero exit value 143 error,
|
||||
// which you may think is a test failure.
|
||||
log.warn("{} Shutting down test case before test completion;"
|
||||
+ " this is not a test failure.",
|
||||
ex.getMessage());
|
||||
} catch (Throwable throwable) {
|
||||
fail(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void tearDown() {
|
||||
if (botScript.isUseTestHarness())
|
||||
tearDownScaffold();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.apitest.scenario.bot;
|
||||
|
||||
import bisq.core.locale.Country;
|
||||
|
||||
import protobuf.PaymentAccount;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static bisq.core.locale.CountryUtil.findCountryByCode;
|
||||
import static bisq.core.payment.payload.PaymentMethod.CLEAR_X_CHANGE_ID;
|
||||
import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById;
|
||||
import static java.lang.String.format;
|
||||
import static java.lang.System.getProperty;
|
||||
import static java.nio.file.Files.readAllBytes;
|
||||
|
||||
|
||||
|
||||
import bisq.apitest.method.MethodTest;
|
||||
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
|
||||
import bisq.apitest.scenario.bot.script.BotScript;
|
||||
|
||||
@Slf4j
|
||||
public abstract class AbstractBotTest extends MethodTest {
|
||||
|
||||
protected static final String BOT_SCRIPT_NAME = "bot-script.json";
|
||||
protected static BotScript botScript;
|
||||
protected static BotClient botClient;
|
||||
|
||||
protected BashScriptGenerator getBashScriptGenerator() {
|
||||
if (botScript.isUseTestHarness()) {
|
||||
PaymentAccount alicesAccount = createAlicesPaymentAccount();
|
||||
botScript.setPaymentAccountIdForCliScripts(alicesAccount.getId());
|
||||
}
|
||||
return new BashScriptGenerator(config.apiPassword,
|
||||
botScript.getApiPortForCliScripts(),
|
||||
botScript.getPaymentAccountIdForCliScripts(),
|
||||
botScript.isPrintCliScripts());
|
||||
}
|
||||
|
||||
private PaymentAccount createAlicesPaymentAccount() {
|
||||
BotPaymentAccountGenerator accountGenerator =
|
||||
new BotPaymentAccountGenerator(new BotClient(aliceClient));
|
||||
String paymentMethodId = botScript.getBotPaymentMethodId();
|
||||
if (paymentMethodId != null) {
|
||||
if (paymentMethodId.equals(CLEAR_X_CHANGE_ID)) {
|
||||
// Only Zelle test accts are supported now.
|
||||
return accountGenerator.createZellePaymentAccount(
|
||||
"Alice's Zelle Account",
|
||||
"Alice");
|
||||
} else {
|
||||
throw new UnsupportedOperationException(
|
||||
format("This test harness bot does not work with %s payment accounts yet.",
|
||||
getPaymentMethodById(paymentMethodId).getDisplayString()));
|
||||
}
|
||||
} else {
|
||||
String countryCode = botScript.getCountryCode();
|
||||
Country country = findCountryByCode(countryCode).orElseThrow(() ->
|
||||
new IllegalArgumentException(countryCode + " is not a valid iso country code."));
|
||||
return accountGenerator.createF2FPaymentAccount(country,
|
||||
"Alice's " + country.name + " F2F Account");
|
||||
}
|
||||
}
|
||||
|
||||
protected static BotScript deserializeBotScript() {
|
||||
try {
|
||||
File botScriptFile = new File(getProperty("java.io.tmpdir"), BOT_SCRIPT_NAME);
|
||||
String json = new String(readAllBytes(Paths.get(botScriptFile.getPath())));
|
||||
return new GsonBuilder().setPrettyPrinting().create().fromJson(json, BotScript.class);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("Error reading script bot file contents.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused") // This is used by the jupiter framework.
|
||||
protected static boolean botScriptExists() {
|
||||
File botScriptFile = new File(getProperty("java.io.tmpdir"), BOT_SCRIPT_NAME);
|
||||
if (botScriptFile.exists()) {
|
||||
botScriptFile.deleteOnExit();
|
||||
log.info("Enabled, found {}.", botScriptFile.getPath());
|
||||
return true;
|
||||
} else {
|
||||
log.info("Skipped, no bot script.\n\tTo generate a bot-script.json file, see BotScriptGenerator.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
77
apitest/src/test/java/bisq/apitest/scenario/bot/Bot.java
Normal file
77
apitest/src/test/java/bisq/apitest/scenario/bot/Bot.java
Normal file
|
@ -0,0 +1,77 @@
|
|||
package bisq.apitest.scenario.bot;
|
||||
|
||||
import bisq.core.locale.Country;
|
||||
|
||||
import protobuf.PaymentAccount;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static bisq.core.locale.CountryUtil.findCountryByCode;
|
||||
import static bisq.core.payment.payload.PaymentMethod.CLEAR_X_CHANGE_ID;
|
||||
import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById;
|
||||
import static java.lang.String.format;
|
||||
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||
|
||||
|
||||
|
||||
import bisq.apitest.method.BitcoinCliHelper;
|
||||
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
|
||||
import bisq.apitest.scenario.bot.script.BotScript;
|
||||
|
||||
@Slf4j
|
||||
public
|
||||
class Bot {
|
||||
|
||||
static final String MAKE = "MAKE";
|
||||
static final String TAKE = "TAKE";
|
||||
|
||||
protected final BotClient botClient;
|
||||
protected final BitcoinCliHelper bitcoinCli;
|
||||
protected final BashScriptGenerator bashScriptGenerator;
|
||||
protected final String[] actions;
|
||||
protected final long protocolStepTimeLimitInMs;
|
||||
protected final boolean stayAlive;
|
||||
protected final boolean isUsingTestHarness;
|
||||
protected final PaymentAccount paymentAccount;
|
||||
|
||||
public Bot(BotClient botClient,
|
||||
BotScript botScript,
|
||||
BitcoinCliHelper bitcoinCli,
|
||||
BashScriptGenerator bashScriptGenerator) {
|
||||
this.botClient = botClient;
|
||||
this.bitcoinCli = bitcoinCli;
|
||||
this.bashScriptGenerator = bashScriptGenerator;
|
||||
this.actions = botScript.getActions();
|
||||
this.protocolStepTimeLimitInMs = MINUTES.toMillis(botScript.getProtocolStepTimeLimitInMinutes());
|
||||
this.stayAlive = botScript.isStayAlive();
|
||||
this.isUsingTestHarness = botScript.isUseTestHarness();
|
||||
if (isUsingTestHarness)
|
||||
this.paymentAccount = createBotPaymentAccount(botScript);
|
||||
else
|
||||
this.paymentAccount = botClient.getPaymentAccount(botScript.getPaymentAccountIdForBot());
|
||||
}
|
||||
|
||||
private PaymentAccount createBotPaymentAccount(BotScript botScript) {
|
||||
BotPaymentAccountGenerator accountGenerator = new BotPaymentAccountGenerator(botClient);
|
||||
|
||||
String paymentMethodId = botScript.getBotPaymentMethodId();
|
||||
if (paymentMethodId != null) {
|
||||
if (paymentMethodId.equals(CLEAR_X_CHANGE_ID)) {
|
||||
return accountGenerator.createZellePaymentAccount("Bob's Zelle Account",
|
||||
"Bob");
|
||||
} else {
|
||||
throw new UnsupportedOperationException(
|
||||
format("This bot test does not work with %s payment accounts yet.",
|
||||
getPaymentMethodById(paymentMethodId).getDisplayString()));
|
||||
}
|
||||
} else {
|
||||
Country country = findCountry(botScript.getCountryCode());
|
||||
return accountGenerator.createF2FPaymentAccount(country, country.name + " F2F Account");
|
||||
}
|
||||
}
|
||||
|
||||
private Country findCountry(String countryCode) {
|
||||
return findCountryByCode(countryCode).orElseThrow(() ->
|
||||
new IllegalArgumentException(countryCode + " is not a valid iso country code."));
|
||||
}
|
||||
}
|
339
apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java
Normal file
339
apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java
Normal file
|
@ -0,0 +1,339 @@
|
|||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.apitest.scenario.bot;
|
||||
|
||||
import bisq.proto.grpc.BalancesInfo;
|
||||
import bisq.proto.grpc.GetPaymentAccountsRequest;
|
||||
import bisq.proto.grpc.OfferInfo;
|
||||
import bisq.proto.grpc.TradeInfo;
|
||||
|
||||
import protobuf.PaymentAccount;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.capitalize;
|
||||
|
||||
|
||||
|
||||
import bisq.cli.GrpcClient;
|
||||
|
||||
/**
|
||||
* Convenience GrpcClient wrapper for bots using gRPC services.
|
||||
*
|
||||
* TODO Consider if the duplication smell is bad enough to force a BotClient user
|
||||
* to use the GrpcClient instead (and delete this class). But right now, I think it is
|
||||
* OK because moving some of the non-gRPC related methods to GrpcClient is even smellier.
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings({"JavaDoc", "unused"})
|
||||
@Slf4j
|
||||
public class BotClient {
|
||||
|
||||
private static final DecimalFormat FIXED_PRICE_FMT = new DecimalFormat("###########0");
|
||||
|
||||
private final GrpcClient grpcClient;
|
||||
|
||||
public BotClient(GrpcClient grpcClient) {
|
||||
this.grpcClient = grpcClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current BSQ and BTC balance information.
|
||||
* @return BalancesInfo
|
||||
*/
|
||||
public BalancesInfo getBalance() {
|
||||
return grpcClient.getBalances();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent BTC market price for the given currencyCode.
|
||||
* @param currencyCode
|
||||
* @return double
|
||||
*/
|
||||
public double getCurrentBTCMarketPrice(String currencyCode) {
|
||||
return grpcClient.getBtcPrice(currencyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent BTC market price for the given currencyCode as an integer string.
|
||||
* @param currencyCode
|
||||
* @return String
|
||||
*/
|
||||
public String getCurrentBTCMarketPriceAsIntegerString(String currencyCode) {
|
||||
return FIXED_PRICE_FMT.format(getCurrentBTCMarketPrice(currencyCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all BUY and SELL offers for the given currencyCode.
|
||||
* @param currencyCode
|
||||
* @return List<OfferInfo>
|
||||
*/
|
||||
public List<OfferInfo> getOffers(String currencyCode) {
|
||||
var buyOffers = getBuyOffers(currencyCode);
|
||||
if (buyOffers.size() > 0) {
|
||||
return buyOffers;
|
||||
} else {
|
||||
return getSellOffers(currencyCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return BUY offers for the given currencyCode.
|
||||
* @param currencyCode
|
||||
* @return List<OfferInfo>
|
||||
*/
|
||||
public List<OfferInfo> getBuyOffers(String currencyCode) {
|
||||
return grpcClient.getOffers("BUY", currencyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return SELL offers for the given currencyCode.
|
||||
* @param currencyCode
|
||||
* @return List<OfferInfo>
|
||||
*/
|
||||
public List<OfferInfo> getSellOffers(String currencyCode) {
|
||||
return grpcClient.getOffers("SELL", currencyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new Offer using a market based price.
|
||||
* @param paymentAccount
|
||||
* @param direction
|
||||
* @param currencyCode
|
||||
* @param amountInSatoshis
|
||||
* @param minAmountInSatoshis
|
||||
* @param priceMarginAsPercent
|
||||
* @param securityDepositAsPercent
|
||||
* @param feeCurrency
|
||||
* @return OfferInfo
|
||||
*/
|
||||
public OfferInfo createOfferAtMarketBasedPrice(PaymentAccount paymentAccount,
|
||||
String direction,
|
||||
String currencyCode,
|
||||
long amountInSatoshis,
|
||||
long minAmountInSatoshis,
|
||||
double priceMarginAsPercent,
|
||||
double securityDepositAsPercent,
|
||||
String feeCurrency) {
|
||||
return grpcClient.createMarketBasedPricedOffer(direction,
|
||||
currencyCode,
|
||||
amountInSatoshis,
|
||||
minAmountInSatoshis,
|
||||
priceMarginAsPercent,
|
||||
securityDepositAsPercent,
|
||||
paymentAccount.getId(),
|
||||
feeCurrency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new Offer using a fixed price.
|
||||
* @param paymentAccount
|
||||
* @param direction
|
||||
* @param currencyCode
|
||||
* @param amountInSatoshis
|
||||
* @param minAmountInSatoshis
|
||||
* @param fixedOfferPriceAsString
|
||||
* @param securityDepositAsPercent
|
||||
* @param feeCurrency
|
||||
* @return OfferInfo
|
||||
*/
|
||||
public OfferInfo createOfferAtFixedPrice(PaymentAccount paymentAccount,
|
||||
String direction,
|
||||
String currencyCode,
|
||||
long amountInSatoshis,
|
||||
long minAmountInSatoshis,
|
||||
String fixedOfferPriceAsString,
|
||||
double securityDepositAsPercent,
|
||||
String feeCurrency) {
|
||||
return grpcClient.createFixedPricedOffer(direction,
|
||||
currencyCode,
|
||||
amountInSatoshis,
|
||||
minAmountInSatoshis,
|
||||
fixedOfferPriceAsString,
|
||||
securityDepositAsPercent,
|
||||
paymentAccount.getId(),
|
||||
feeCurrency);
|
||||
}
|
||||
|
||||
public TradeInfo takeOffer(String offerId, PaymentAccount paymentAccount, String feeCurrency) {
|
||||
return grpcClient.takeOffer(offerId, paymentAccount.getId(), feeCurrency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a persisted Trade with the given tradeId, or throws an exception.
|
||||
* @param tradeId
|
||||
* @return TradeInfo
|
||||
*/
|
||||
public TradeInfo getTrade(String tradeId) {
|
||||
return grpcClient.getTrade(tradeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Predicate returns true if the given exception indicates the trade with the given
|
||||
* tradeId exists, but the trade's contract has not been fully prepared.
|
||||
*/
|
||||
public final BiPredicate<Exception, String> tradeContractIsNotReady = (exception, tradeId) -> {
|
||||
if (exception.getMessage().contains("no contract was found")) {
|
||||
log.warn("Trade {} exists but is not fully prepared: {}.",
|
||||
tradeId,
|
||||
toCleanGrpcExceptionMessage(exception));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a trade's contract as a Json string, or null if the trade exists
|
||||
* but the contract is not ready.
|
||||
* @param tradeId
|
||||
* @return String
|
||||
*/
|
||||
public String getTradeContract(String tradeId) {
|
||||
try {
|
||||
var trade = grpcClient.getTrade(tradeId);
|
||||
return trade.getContractAsJson();
|
||||
} catch (Exception ex) {
|
||||
if (tradeContractIsNotReady.test(ex, tradeId))
|
||||
return null;
|
||||
else
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the trade's taker deposit fee transaction has been published.
|
||||
* @param tradeId a valid trade id
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isTakerDepositFeeTxPublished(String tradeId) {
|
||||
return grpcClient.getTrade(tradeId).getIsPayoutPublished();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the trade's taker deposit fee transaction has been confirmed.
|
||||
* @param tradeId a valid trade id
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isTakerDepositFeeTxConfirmed(String tradeId) {
|
||||
return grpcClient.getTrade(tradeId).getIsDepositConfirmed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the trade's 'start payment' message has been sent by the buyer.
|
||||
* @param tradeId a valid trade id
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isTradePaymentStartedSent(String tradeId) {
|
||||
return grpcClient.getTrade(tradeId).getIsFiatSent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the trade's 'payment received' message has been sent by the seller.
|
||||
* @param tradeId a valid trade id
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isTradePaymentReceivedConfirmationSent(String tradeId) {
|
||||
return grpcClient.getTrade(tradeId).getIsFiatReceived();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the trade's payout transaction has been published.
|
||||
* @param tradeId a valid trade id
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isTradePayoutTxPublished(String tradeId) {
|
||||
return grpcClient.getTrade(tradeId).getIsPayoutPublished();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a 'confirm payment started message' for a trade with the given tradeId,
|
||||
* or throws an exception.
|
||||
* @param tradeId
|
||||
*/
|
||||
public void sendConfirmPaymentStartedMessage(String tradeId) {
|
||||
grpcClient.confirmPaymentStarted(tradeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a 'confirm payment received message' for a trade with the given tradeId,
|
||||
* or throws an exception.
|
||||
* @param tradeId
|
||||
*/
|
||||
public void sendConfirmPaymentReceivedMessage(String tradeId) {
|
||||
grpcClient.confirmPaymentReceived(tradeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a 'keep funds in wallet message' for a trade with the given tradeId,
|
||||
* or throws an exception.
|
||||
* @param tradeId
|
||||
*/
|
||||
public void sendKeepFundsMessage(String tradeId) {
|
||||
grpcClient.keepFunds(tradeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and save a new PaymentAccount with details in the given json.
|
||||
* @param json
|
||||
* @return PaymentAccount
|
||||
*/
|
||||
public PaymentAccount createNewPaymentAccount(String json) {
|
||||
return grpcClient.createPaymentAccount(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a persisted PaymentAccount with the given paymentAccountId, or throws
|
||||
* an exception.
|
||||
* @param paymentAccountId The id of the PaymentAccount being looked up.
|
||||
* @return PaymentAccount
|
||||
*/
|
||||
public PaymentAccount getPaymentAccount(String paymentAccountId) {
|
||||
return grpcClient.getPaymentAccounts().stream()
|
||||
.filter(a -> (a.getId().equals(paymentAccountId)))
|
||||
.findFirst()
|
||||
.orElseThrow(() ->
|
||||
new PaymentAccountNotFoundException("Could not find a payment account with id "
|
||||
+ paymentAccountId + "."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a persisted PaymentAccount with the given accountName, or throws
|
||||
* an exception.
|
||||
* @param accountName
|
||||
* @return PaymentAccount
|
||||
*/
|
||||
public PaymentAccount getPaymentAccountWithName(String accountName) {
|
||||
var req = GetPaymentAccountsRequest.newBuilder().build();
|
||||
return grpcClient.getPaymentAccounts().stream()
|
||||
.filter(a -> (a.getAccountName().equals(accountName)))
|
||||
.findFirst()
|
||||
.orElseThrow(() ->
|
||||
new PaymentAccountNotFoundException("Could not find a payment account with name "
|
||||
+ accountName + "."));
|
||||
}
|
||||
|
||||
public String toCleanGrpcExceptionMessage(Exception ex) {
|
||||
return capitalize(ex.getMessage().replaceFirst("^[A-Z_]+: ", ""));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package bisq.apitest.scenario.bot;
|
||||
|
||||
import bisq.core.api.model.PaymentAccountForm;
|
||||
import bisq.core.locale.Country;
|
||||
|
||||
import protobuf.PaymentAccount;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static bisq.core.payment.payload.PaymentMethod.CLEAR_X_CHANGE_ID;
|
||||
import static bisq.core.payment.payload.PaymentMethod.F2F_ID;
|
||||
|
||||
@Slf4j
|
||||
public class BotPaymentAccountGenerator {
|
||||
|
||||
private final Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
|
||||
|
||||
private final BotClient botClient;
|
||||
|
||||
public BotPaymentAccountGenerator(BotClient botClient) {
|
||||
this.botClient = botClient;
|
||||
}
|
||||
|
||||
public PaymentAccount createF2FPaymentAccount(Country country, String accountName) {
|
||||
try {
|
||||
return botClient.getPaymentAccountWithName(accountName);
|
||||
} catch (PaymentAccountNotFoundException ignored) {
|
||||
// Ignore not found exception, create a new account.
|
||||
}
|
||||
Map<String, Object> p = getPaymentAccountFormMap(F2F_ID);
|
||||
p.put("accountName", accountName);
|
||||
p.put("city", country.name + " City");
|
||||
p.put("country", country.code);
|
||||
p.put("contact", "By Semaphore");
|
||||
p.put("extraInfo", "");
|
||||
// Convert the map back to a json string and create the payment account over gRPC.
|
||||
return botClient.createNewPaymentAccount(gson.toJson(p));
|
||||
}
|
||||
|
||||
public PaymentAccount createZellePaymentAccount(String accountName, String holderName) {
|
||||
try {
|
||||
return botClient.getPaymentAccountWithName(accountName);
|
||||
} catch (PaymentAccountNotFoundException ignored) {
|
||||
// Ignore not found exception, create a new account.
|
||||
}
|
||||
Map<String, Object> p = getPaymentAccountFormMap(CLEAR_X_CHANGE_ID);
|
||||
p.put("accountName", accountName);
|
||||
p.put("emailOrMobileNr", holderName + "@zelle.com");
|
||||
p.put("holderName", holderName);
|
||||
return botClient.createNewPaymentAccount(gson.toJson(p));
|
||||
}
|
||||
|
||||
private Map<String, Object> getPaymentAccountFormMap(String paymentMethodId) {
|
||||
PaymentAccountForm paymentAccountForm = new PaymentAccountForm();
|
||||
File jsonFormTemplate = paymentAccountForm.getPaymentAccountForm(paymentMethodId);
|
||||
jsonFormTemplate.deleteOnExit();
|
||||
String jsonString = paymentAccountForm.toJsonString(jsonFormTemplate);
|
||||
//noinspection unchecked
|
||||
return (Map<String, Object>) gson.fromJson(jsonString, Object.class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.apitest.scenario.bot;
|
||||
|
||||
import bisq.common.BisqException;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class InvalidRandomOfferException extends BisqException {
|
||||
public InvalidRandomOfferException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public InvalidRandomOfferException(String format, Object... args) {
|
||||
super(format, args);
|
||||
}
|
||||
|
||||
public InvalidRandomOfferException(Throwable cause, String format, Object... args) {
|
||||
super(cause, format, args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.apitest.scenario.bot;
|
||||
|
||||
import bisq.common.BisqException;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class PaymentAccountNotFoundException extends BisqException {
|
||||
public PaymentAccountNotFoundException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public PaymentAccountNotFoundException(String format, Object... args) {
|
||||
super(format, args);
|
||||
}
|
||||
|
||||
public PaymentAccountNotFoundException(Throwable cause, String format, Object... args) {
|
||||
super(cause, format, args);
|
||||
}
|
||||
}
|
177
apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java
Normal file
177
apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java
Normal file
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.apitest.scenario.bot;
|
||||
|
||||
import bisq.proto.grpc.OfferInfo;
|
||||
|
||||
import protobuf.PaymentAccount;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static bisq.cli.CurrencyFormat.formatMarketPrice;
|
||||
import static bisq.cli.CurrencyFormat.formatSatoshis;
|
||||
import static bisq.common.util.MathUtils.scaleDownByPowerOf10;
|
||||
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
|
||||
import static bisq.core.payment.payload.PaymentMethod.F2F_ID;
|
||||
import static java.lang.String.format;
|
||||
import static java.math.RoundingMode.HALF_UP;
|
||||
|
||||
@Slf4j
|
||||
public class RandomOffer {
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
private static final DecimalFormat FIXED_PRICE_FMT = new DecimalFormat("###########0");
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
// If not an F2F account, keep amount <= 0.01 BTC to avoid hitting unsigned
|
||||
// acct trading limit.
|
||||
private final Supplier<Long> nextAmount = () ->
|
||||
this.getPaymentAccount().getPaymentMethod().getId().equals(F2F_ID)
|
||||
? (long) (10000000 + RANDOM.nextInt(2500000))
|
||||
: (long) (750000 + RANDOM.nextInt(250000));
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final Supplier<Long> nextMinAmount = () -> {
|
||||
boolean useMinAmount = RANDOM.nextBoolean();
|
||||
if (useMinAmount) {
|
||||
return this.getPaymentAccount().getPaymentMethod().getId().equals(F2F_ID)
|
||||
? this.getAmount() - 5000000L
|
||||
: this.getAmount() - 50000L;
|
||||
} else {
|
||||
return this.getAmount();
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final Supplier<Double> nextPriceMargin = () -> {
|
||||
boolean useZeroMargin = RANDOM.nextBoolean();
|
||||
if (useZeroMargin) {
|
||||
return 0.00;
|
||||
} else {
|
||||
BigDecimal min = BigDecimal.valueOf(-5.0).setScale(2, HALF_UP);
|
||||
BigDecimal max = BigDecimal.valueOf(5.0).setScale(2, HALF_UP);
|
||||
BigDecimal randomBigDecimal = min.add(BigDecimal.valueOf(RANDOM.nextDouble()).multiply(max.subtract(min)));
|
||||
return randomBigDecimal.setScale(2, HALF_UP).doubleValue();
|
||||
}
|
||||
};
|
||||
|
||||
private final BotClient botClient;
|
||||
@Getter
|
||||
private final PaymentAccount paymentAccount;
|
||||
@Getter
|
||||
private final String direction;
|
||||
@Getter
|
||||
private final String currencyCode;
|
||||
@Getter
|
||||
private final long amount;
|
||||
@Getter
|
||||
private final long minAmount;
|
||||
@Getter
|
||||
private final boolean useMarketBasedPrice;
|
||||
@Getter
|
||||
private final double priceMargin;
|
||||
@Getter
|
||||
private final String feeCurrency;
|
||||
|
||||
@Getter
|
||||
private String fixedOfferPrice = "0";
|
||||
@Getter
|
||||
private OfferInfo offer;
|
||||
@Getter
|
||||
private String id;
|
||||
|
||||
public RandomOffer(BotClient botClient, PaymentAccount paymentAccount) {
|
||||
this.botClient = botClient;
|
||||
this.paymentAccount = paymentAccount;
|
||||
this.direction = RANDOM.nextBoolean() ? "BUY" : "SELL";
|
||||
this.currencyCode = Objects.requireNonNull(paymentAccount.getSelectedTradeCurrency()).getCode();
|
||||
this.amount = nextAmount.get();
|
||||
this.minAmount = nextMinAmount.get();
|
||||
this.useMarketBasedPrice = RANDOM.nextBoolean();
|
||||
this.priceMargin = nextPriceMargin.get();
|
||||
this.feeCurrency = RANDOM.nextBoolean() ? "BSQ" : "BTC";
|
||||
}
|
||||
|
||||
public RandomOffer create() throws InvalidRandomOfferException {
|
||||
try {
|
||||
printDescription();
|
||||
if (useMarketBasedPrice) {
|
||||
this.offer = botClient.createOfferAtMarketBasedPrice(paymentAccount,
|
||||
direction,
|
||||
currencyCode,
|
||||
amount,
|
||||
minAmount,
|
||||
priceMargin,
|
||||
getDefaultBuyerSecurityDepositAsPercent(),
|
||||
feeCurrency);
|
||||
} else {
|
||||
this.offer = botClient.createOfferAtFixedPrice(paymentAccount,
|
||||
direction,
|
||||
currencyCode,
|
||||
amount,
|
||||
minAmount,
|
||||
fixedOfferPrice,
|
||||
getDefaultBuyerSecurityDepositAsPercent(),
|
||||
feeCurrency);
|
||||
}
|
||||
this.id = offer.getId();
|
||||
return this;
|
||||
} catch (Exception ex) {
|
||||
String error = format("Could not create valid %s offer for %s BTC: %s",
|
||||
currencyCode,
|
||||
formatSatoshis(amount),
|
||||
ex.getMessage());
|
||||
throw new InvalidRandomOfferException(error, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void printDescription() {
|
||||
double currentMarketPrice = botClient.getCurrentBTCMarketPrice(currencyCode);
|
||||
// Calculate a fixed price based on the random mkt price margin, even if we don't use it.
|
||||
double differenceFromMarketPrice = currentMarketPrice * scaleDownByPowerOf10(priceMargin, 2);
|
||||
double fixedOfferPriceAsDouble = direction.equals("BUY")
|
||||
? currentMarketPrice - differenceFromMarketPrice
|
||||
: currentMarketPrice + differenceFromMarketPrice;
|
||||
this.fixedOfferPrice = FIXED_PRICE_FMT.format(fixedOfferPriceAsDouble);
|
||||
String description = format("Creating new %s %s / %s offer for amount = %s BTC, min-amount = %s BTC.",
|
||||
useMarketBasedPrice ? "mkt-based-price" : "fixed-priced",
|
||||
direction,
|
||||
currencyCode,
|
||||
formatSatoshis(amount),
|
||||
formatSatoshis(minAmount));
|
||||
log.info(description);
|
||||
if (useMarketBasedPrice) {
|
||||
log.info("Offer Price Margin = {}%", priceMargin);
|
||||
log.info("Expected Offer Price = {} {}", formatMarketPrice(Double.parseDouble(fixedOfferPrice)), currencyCode);
|
||||
} else {
|
||||
|
||||
log.info("Fixed Offer Price = {} {}", fixedOfferPrice, currencyCode);
|
||||
}
|
||||
log.info("Current Market Price = {} {}", formatMarketPrice(currentMarketPrice), currencyCode);
|
||||
}
|
||||
}
|
141
apitest/src/test/java/bisq/apitest/scenario/bot/RobotBob.java
Normal file
141
apitest/src/test/java/bisq/apitest/scenario/bot/RobotBob.java
Normal file
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.apitest.scenario.bot;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE;
|
||||
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.isShutdownCalled;
|
||||
import static bisq.cli.TableFormat.formatBalancesTbls;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
|
||||
|
||||
import bisq.apitest.method.BitcoinCliHelper;
|
||||
import bisq.apitest.scenario.bot.protocol.BotProtocol;
|
||||
import bisq.apitest.scenario.bot.protocol.MakerBotProtocol;
|
||||
import bisq.apitest.scenario.bot.protocol.TakerBotProtocol;
|
||||
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
|
||||
import bisq.apitest.scenario.bot.script.BotScript;
|
||||
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
|
||||
|
||||
@Slf4j
|
||||
public
|
||||
class RobotBob extends Bot {
|
||||
|
||||
@Getter
|
||||
private int numTrades;
|
||||
|
||||
public RobotBob(BotClient botClient,
|
||||
BotScript botScript,
|
||||
BitcoinCliHelper bitcoinCli,
|
||||
BashScriptGenerator bashScriptGenerator) {
|
||||
super(botClient, botScript, bitcoinCli, bashScriptGenerator);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
for (String action : actions) {
|
||||
checkActionIsValid(action);
|
||||
|
||||
BotProtocol botProtocol;
|
||||
if (action.equalsIgnoreCase(MAKE)) {
|
||||
botProtocol = new MakerBotProtocol(botClient,
|
||||
paymentAccount,
|
||||
protocolStepTimeLimitInMs,
|
||||
bitcoinCli,
|
||||
bashScriptGenerator);
|
||||
} else {
|
||||
botProtocol = new TakerBotProtocol(botClient,
|
||||
paymentAccount,
|
||||
protocolStepTimeLimitInMs,
|
||||
bitcoinCli,
|
||||
bashScriptGenerator);
|
||||
}
|
||||
|
||||
botProtocol.run();
|
||||
|
||||
if (!botProtocol.getCurrentProtocolStep().equals(DONE)) {
|
||||
throw new IllegalStateException(botProtocol.getClass().getSimpleName() + " failed to complete.");
|
||||
}
|
||||
|
||||
log.info("Completed {} successful trade{}. Current Balance:\n{}",
|
||||
++numTrades,
|
||||
numTrades == 1 ? "" : "s",
|
||||
formatBalancesTbls(botClient.getBalance()));
|
||||
|
||||
if (numTrades < actions.length) {
|
||||
try {
|
||||
SECONDS.sleep(20);
|
||||
} catch (InterruptedException ignored) {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
|
||||
} // end of actions loop
|
||||
|
||||
if (stayAlive)
|
||||
waitForManualShutdown();
|
||||
else
|
||||
warnCLIUserBeforeShutdown();
|
||||
}
|
||||
|
||||
private void checkActionIsValid(String action) {
|
||||
if (!action.equalsIgnoreCase(MAKE) && !action.equalsIgnoreCase(TAKE))
|
||||
throw new IllegalStateException(action + " is not a valid bot action; must be 'make' or 'take'");
|
||||
}
|
||||
|
||||
private void waitForManualShutdown() {
|
||||
String harnessOrCase = isUsingTestHarness ? "harness" : "case";
|
||||
log.info("All script actions have been completed, but the test {} will stay alive"
|
||||
+ " until a /tmp/bottest-shutdown file is detected.",
|
||||
harnessOrCase);
|
||||
log.info("When ready to shutdown the test {}, run '$ touch /tmp/bottest-shutdown'.",
|
||||
harnessOrCase);
|
||||
if (!isUsingTestHarness) {
|
||||
log.warn("You will have to manually shutdown the bitcoind and Bisq nodes"
|
||||
+ " running outside of the test harness.");
|
||||
}
|
||||
try {
|
||||
while (!isShutdownCalled()) {
|
||||
SECONDS.sleep(10);
|
||||
}
|
||||
log.warn("Manual shutdown signal received.");
|
||||
} catch (ManualBotShutdownException ex) {
|
||||
log.warn(ex.getMessage());
|
||||
} catch (InterruptedException ignored) {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
|
||||
private void warnCLIUserBeforeShutdown() {
|
||||
if (isUsingTestHarness) {
|
||||
long delayInSeconds = 30;
|
||||
log.warn("All script actions have been completed. You have {} seconds to complete any"
|
||||
+ " remaining tasks before the test harness shuts down.",
|
||||
delayInSeconds);
|
||||
try {
|
||||
SECONDS.sleep(delayInSeconds);
|
||||
} catch (InterruptedException ignored) {
|
||||
// empty
|
||||
}
|
||||
} else {
|
||||
log.info("Shutting down test case");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,349 @@
|
|||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.apitest.scenario.bot.protocol;
|
||||
|
||||
|
||||
import bisq.proto.grpc.TradeInfo;
|
||||
|
||||
import protobuf.PaymentAccount;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.*;
|
||||
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
|
||||
import static java.lang.String.format;
|
||||
import static java.lang.System.currentTimeMillis;
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
|
||||
|
||||
import bisq.apitest.method.BitcoinCliHelper;
|
||||
import bisq.apitest.scenario.bot.BotClient;
|
||||
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
|
||||
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
|
||||
import bisq.cli.TradeFormat;
|
||||
|
||||
@Slf4j
|
||||
public abstract class BotProtocol {
|
||||
|
||||
static final SecureRandom RANDOM = new SecureRandom();
|
||||
static final String BUY = "BUY";
|
||||
static final String SELL = "SELL";
|
||||
|
||||
protected final Supplier<Long> randomDelay = () -> (long) (2000 + RANDOM.nextInt(5000));
|
||||
|
||||
protected final AtomicLong protocolStepStartTime = new AtomicLong(0);
|
||||
protected final Consumer<ProtocolStep> initProtocolStep = (step) -> {
|
||||
currentProtocolStep = step;
|
||||
printBotProtocolStep();
|
||||
protocolStepStartTime.set(currentTimeMillis());
|
||||
};
|
||||
|
||||
@Getter
|
||||
protected ProtocolStep currentProtocolStep;
|
||||
|
||||
@Getter // Functions within 'this' need the @Getter.
|
||||
protected final BotClient botClient;
|
||||
protected final PaymentAccount paymentAccount;
|
||||
protected final String currencyCode;
|
||||
protected final long protocolStepTimeLimitInMs;
|
||||
protected final BitcoinCliHelper bitcoinCli;
|
||||
@Getter
|
||||
protected final BashScriptGenerator bashScriptGenerator;
|
||||
|
||||
public BotProtocol(BotClient botClient,
|
||||
PaymentAccount paymentAccount,
|
||||
long protocolStepTimeLimitInMs,
|
||||
BitcoinCliHelper bitcoinCli,
|
||||
BashScriptGenerator bashScriptGenerator) {
|
||||
this.botClient = botClient;
|
||||
this.paymentAccount = paymentAccount;
|
||||
this.currencyCode = Objects.requireNonNull(paymentAccount.getSelectedTradeCurrency()).getCode();
|
||||
this.protocolStepTimeLimitInMs = protocolStepTimeLimitInMs;
|
||||
this.bitcoinCli = bitcoinCli;
|
||||
this.bashScriptGenerator = bashScriptGenerator;
|
||||
this.currentProtocolStep = START;
|
||||
}
|
||||
|
||||
public abstract void run();
|
||||
|
||||
protected boolean isWithinProtocolStepTimeLimit() {
|
||||
return (currentTimeMillis() - protocolStepStartTime.get()) < protocolStepTimeLimitInMs;
|
||||
}
|
||||
|
||||
protected void checkIsStartStep() {
|
||||
if (currentProtocolStep != START) {
|
||||
throw new IllegalStateException("First bot protocol step must be " + START.name());
|
||||
}
|
||||
}
|
||||
|
||||
protected void printBotProtocolStep() {
|
||||
log.info("Starting protocol step {}. Bot will shutdown if step not completed within {} minutes.",
|
||||
currentProtocolStep.name(), MILLISECONDS.toMinutes(protocolStepTimeLimitInMs));
|
||||
|
||||
if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED)) {
|
||||
log.info("Generate a btc block to trigger taker's deposit fee tx confirmation.");
|
||||
createGenerateBtcBlockScript();
|
||||
}
|
||||
}
|
||||
|
||||
protected final Function<TradeInfo, TradeInfo> waitForTakerFeeTxConfirm = (trade) -> {
|
||||
sleep(5000);
|
||||
waitForTakerFeeTxPublished(trade.getTradeId());
|
||||
waitForTakerFeeTxConfirmed(trade.getTradeId());
|
||||
return trade;
|
||||
};
|
||||
|
||||
protected final Function<TradeInfo, TradeInfo> waitForPaymentStartedMessage = (trade) -> {
|
||||
initProtocolStep.accept(WAIT_FOR_PAYMENT_STARTED_MESSAGE);
|
||||
try {
|
||||
createPaymentStartedScript(trade);
|
||||
log.info(" Waiting for a 'payment started' message from buyer for trade with id {}.", trade.getTradeId());
|
||||
while (isWithinProtocolStepTimeLimit()) {
|
||||
checkIfShutdownCalled("Interrupted before checking if 'payment started' message has been sent.");
|
||||
try {
|
||||
var t = this.getBotClient().getTrade(trade.getTradeId());
|
||||
if (t.getIsFiatSent()) {
|
||||
log.info("Buyer has started payment for trade:\n{}", TradeFormat.format(t));
|
||||
return t;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex));
|
||||
}
|
||||
sleep(randomDelay.get());
|
||||
} // end while
|
||||
|
||||
throw new IllegalStateException("Payment was never sent; we won't wait any longer.");
|
||||
} catch (ManualBotShutdownException ex) {
|
||||
throw ex; // not an error, tells bot to shutdown
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Error while waiting payment sent message.", ex);
|
||||
}
|
||||
};
|
||||
|
||||
protected final Function<TradeInfo, TradeInfo> sendPaymentStartedMessage = (trade) -> {
|
||||
initProtocolStep.accept(SEND_PAYMENT_STARTED_MESSAGE);
|
||||
checkIfShutdownCalled("Interrupted before sending 'payment started' message.");
|
||||
this.getBotClient().sendConfirmPaymentStartedMessage(trade.getTradeId());
|
||||
return trade;
|
||||
};
|
||||
|
||||
protected final Function<TradeInfo, TradeInfo> waitForPaymentReceivedConfirmation = (trade) -> {
|
||||
initProtocolStep.accept(WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE);
|
||||
createPaymentReceivedScript(trade);
|
||||
try {
|
||||
log.info("Waiting for a 'payment received confirmation' message from seller for trade with id {}.", trade.getTradeId());
|
||||
while (isWithinProtocolStepTimeLimit()) {
|
||||
checkIfShutdownCalled("Interrupted before checking if 'payment received confirmation' message has been sent.");
|
||||
try {
|
||||
var t = this.getBotClient().getTrade(trade.getTradeId());
|
||||
if (t.getIsFiatReceived()) {
|
||||
log.info("Seller has received payment for trade:\n{}", TradeFormat.format(t));
|
||||
return t;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex));
|
||||
}
|
||||
sleep(randomDelay.get());
|
||||
} // end while
|
||||
|
||||
throw new IllegalStateException("Payment was never received; we won't wait any longer.");
|
||||
} catch (ManualBotShutdownException ex) {
|
||||
throw ex; // not an error, tells bot to shutdown
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Error while waiting payment received confirmation message.", ex);
|
||||
}
|
||||
};
|
||||
|
||||
protected final Function<TradeInfo, TradeInfo> sendPaymentReceivedMessage = (trade) -> {
|
||||
initProtocolStep.accept(SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE);
|
||||
checkIfShutdownCalled("Interrupted before sending 'payment received confirmation' message.");
|
||||
this.getBotClient().sendConfirmPaymentReceivedMessage(trade.getTradeId());
|
||||
return trade;
|
||||
};
|
||||
|
||||
protected final Function<TradeInfo, TradeInfo> waitForPayoutTx = (trade) -> {
|
||||
initProtocolStep.accept(WAIT_FOR_PAYOUT_TX);
|
||||
try {
|
||||
log.info("Waiting on the 'payout tx published confirmation' for trade with id {}.", trade.getTradeId());
|
||||
while (isWithinProtocolStepTimeLimit()) {
|
||||
checkIfShutdownCalled("Interrupted before checking if payout tx has been published.");
|
||||
try {
|
||||
var t = this.getBotClient().getTrade(trade.getTradeId());
|
||||
if (t.getIsPayoutPublished()) {
|
||||
log.info("Payout tx {} has been published for trade:\n{}",
|
||||
t.getPayoutTxId(),
|
||||
TradeFormat.format(t));
|
||||
return t;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex));
|
||||
}
|
||||
sleep(randomDelay.get());
|
||||
} // end while
|
||||
|
||||
throw new IllegalStateException("Payout tx was never published; we won't wait any longer.");
|
||||
} catch (ManualBotShutdownException ex) {
|
||||
throw ex; // not an error, tells bot to shutdown
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Error while waiting for published payout tx.", ex);
|
||||
}
|
||||
};
|
||||
|
||||
protected final Function<TradeInfo, TradeInfo> keepFundsFromTrade = (trade) -> {
|
||||
initProtocolStep.accept(KEEP_FUNDS);
|
||||
var isBuy = trade.getOffer().getDirection().equalsIgnoreCase(BUY);
|
||||
var isSell = trade.getOffer().getDirection().equalsIgnoreCase(SELL);
|
||||
var cliUserIsSeller = (this instanceof MakerBotProtocol && isBuy) || (this instanceof TakerBotProtocol && isSell);
|
||||
if (cliUserIsSeller) {
|
||||
createKeepFundsScript(trade);
|
||||
} else {
|
||||
createGetBalanceScript();
|
||||
}
|
||||
checkIfShutdownCalled("Interrupted before closing trade with 'keep funds' command.");
|
||||
this.getBotClient().sendKeepFundsMessage(trade.getTradeId());
|
||||
return trade;
|
||||
};
|
||||
|
||||
protected void createPaymentStartedScript(TradeInfo trade) {
|
||||
File script = bashScriptGenerator.createPaymentStartedScript(trade);
|
||||
printCliHintAndOrScript(script, "The manual CLI side can send a 'payment started' message");
|
||||
}
|
||||
|
||||
protected void createPaymentReceivedScript(TradeInfo trade) {
|
||||
File script = bashScriptGenerator.createPaymentReceivedScript(trade);
|
||||
printCliHintAndOrScript(script, "The manual CLI side can sent a 'payment received confirmation' message");
|
||||
}
|
||||
|
||||
protected void createKeepFundsScript(TradeInfo trade) {
|
||||
File script = bashScriptGenerator.createKeepFundsScript(trade);
|
||||
printCliHintAndOrScript(script, "The manual CLI side can close the trade");
|
||||
}
|
||||
|
||||
protected void createGetBalanceScript() {
|
||||
File script = bashScriptGenerator.createGetBalanceScript();
|
||||
printCliHintAndOrScript(script, "The manual CLI side can view current balances");
|
||||
}
|
||||
|
||||
protected void createGenerateBtcBlockScript() {
|
||||
String newBitcoinCoreAddress = bitcoinCli.getNewBtcAddress();
|
||||
File script = bashScriptGenerator.createGenerateBtcBlockScript(newBitcoinCoreAddress);
|
||||
printCliHintAndOrScript(script, "The manual CLI side can generate 1 btc block");
|
||||
}
|
||||
|
||||
protected void printCliHintAndOrScript(File script, String hint) {
|
||||
log.info("{} by running bash script '{}'.", hint, script.getAbsolutePath());
|
||||
if (this.getBashScriptGenerator().isPrintCliScripts())
|
||||
this.getBashScriptGenerator().printCliScript(script, log);
|
||||
|
||||
sleep(5000); // Allow 5s for CLI user to read the hint.
|
||||
}
|
||||
|
||||
protected void sleep(long ms) {
|
||||
try {
|
||||
MILLISECONDS.sleep(ms);
|
||||
} catch (InterruptedException ignored) {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
|
||||
private void waitForTakerFeeTxPublished(String tradeId) {
|
||||
waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED);
|
||||
}
|
||||
|
||||
private void waitForTakerFeeTxConfirmed(String tradeId) {
|
||||
waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED);
|
||||
}
|
||||
|
||||
private void waitForTakerDepositFee(String tradeId, ProtocolStep depositTxProtocolStep) {
|
||||
initProtocolStep.accept(depositTxProtocolStep);
|
||||
validateCurrentProtocolStep(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED);
|
||||
try {
|
||||
log.info(waitingForDepositFeeTxMsg(tradeId));
|
||||
while (isWithinProtocolStepTimeLimit()) {
|
||||
checkIfShutdownCalled("Interrupted before checking taker deposit fee tx is published and confirmed.");
|
||||
try {
|
||||
var trade = this.getBotClient().getTrade(tradeId);
|
||||
if (isDepositFeeTxStepComplete.test(trade))
|
||||
return;
|
||||
else
|
||||
sleep(randomDelay.get());
|
||||
} catch (Exception ex) {
|
||||
if (this.getBotClient().tradeContractIsNotReady.test(ex, tradeId))
|
||||
sleep(randomDelay.get());
|
||||
else
|
||||
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex));
|
||||
}
|
||||
} // end while
|
||||
throw new IllegalStateException(stoppedWaitingForDepositFeeTxMsg(this.getBotClient().getTrade(tradeId).getDepositTxId()));
|
||||
} catch (ManualBotShutdownException ex) {
|
||||
throw ex; // not an error, tells bot to shutdown
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Error while waiting for taker deposit tx to be published or confirmed.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private final Predicate<TradeInfo> isDepositFeeTxStepComplete = (trade) -> {
|
||||
if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) && trade.getIsDepositPublished()) {
|
||||
log.info("Taker deposit fee tx {} has been published.", trade.getDepositTxId());
|
||||
return true;
|
||||
} else if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED) && trade.getIsDepositConfirmed()) {
|
||||
log.info("Taker deposit fee tx {} has been confirmed.", trade.getDepositTxId());
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
private void validateCurrentProtocolStep(Enum<?>... validBotSteps) {
|
||||
for (Enum<?> validBotStep : validBotSteps) {
|
||||
if (currentProtocolStep.equals(validBotStep))
|
||||
return;
|
||||
}
|
||||
throw new IllegalStateException("Unexpected bot step: " + currentProtocolStep.name() + ".\n"
|
||||
+ "Must be one of "
|
||||
+ stream(validBotSteps).map((Enum::name)).collect(Collectors.joining(","))
|
||||
+ ".");
|
||||
}
|
||||
|
||||
private String waitingForDepositFeeTxMsg(String tradeId) {
|
||||
return format("Waiting for taker deposit fee tx for trade %s to be %s.",
|
||||
tradeId,
|
||||
currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) ? "published" : "confirmed");
|
||||
}
|
||||
|
||||
private String stoppedWaitingForDepositFeeTxMsg(String txId) {
|
||||
return format("Taker deposit fee tx %s is took too long to be %s; we won't wait any longer.",
|
||||
txId,
|
||||
currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) ? "published" : "confirmed");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package bisq.apitest.scenario.bot.protocol;
|
||||
|
||||
import bisq.proto.grpc.OfferInfo;
|
||||
import bisq.proto.grpc.TradeInfo;
|
||||
|
||||
import protobuf.PaymentAccount;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE;
|
||||
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_OFFER_TAKER;
|
||||
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
|
||||
import static bisq.cli.TableFormat.formatOfferTable;
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
|
||||
|
||||
import bisq.apitest.method.BitcoinCliHelper;
|
||||
import bisq.apitest.scenario.bot.BotClient;
|
||||
import bisq.apitest.scenario.bot.RandomOffer;
|
||||
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
|
||||
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
|
||||
import bisq.cli.TradeFormat;
|
||||
|
||||
@Slf4j
|
||||
public class MakerBotProtocol extends BotProtocol {
|
||||
|
||||
public MakerBotProtocol(BotClient botClient,
|
||||
PaymentAccount paymentAccount,
|
||||
long protocolStepTimeLimitInMs,
|
||||
BitcoinCliHelper bitcoinCli,
|
||||
BashScriptGenerator bashScriptGenerator) {
|
||||
super(botClient,
|
||||
paymentAccount,
|
||||
protocolStepTimeLimitInMs,
|
||||
bitcoinCli,
|
||||
bashScriptGenerator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
checkIsStartStep();
|
||||
|
||||
Function<Supplier<OfferInfo>, TradeInfo> makeTrade = waitForNewTrade.andThen(waitForTakerFeeTxConfirm);
|
||||
var trade = makeTrade.apply(randomOffer);
|
||||
|
||||
var makerIsBuyer = trade.getOffer().getDirection().equalsIgnoreCase(BUY);
|
||||
Function<TradeInfo, TradeInfo> completeFiatTransaction = makerIsBuyer
|
||||
? sendPaymentStartedMessage.andThen(waitForPaymentReceivedConfirmation)
|
||||
: waitForPaymentStartedMessage.andThen(sendPaymentReceivedMessage);
|
||||
completeFiatTransaction.apply(trade);
|
||||
|
||||
Function<TradeInfo, TradeInfo> closeTrade = waitForPayoutTx.andThen(keepFundsFromTrade);
|
||||
closeTrade.apply(trade);
|
||||
|
||||
currentProtocolStep = DONE;
|
||||
}
|
||||
|
||||
private final Supplier<OfferInfo> randomOffer = () -> {
|
||||
checkIfShutdownCalled("Interrupted before creating random offer.");
|
||||
OfferInfo offer = new RandomOffer(botClient, paymentAccount).create().getOffer();
|
||||
log.info("Created random {} offer\n{}", currencyCode, formatOfferTable(singletonList(offer), currencyCode));
|
||||
return offer;
|
||||
};
|
||||
|
||||
private final Function<Supplier<OfferInfo>, TradeInfo> waitForNewTrade = (randomOffer) -> {
|
||||
initProtocolStep.accept(WAIT_FOR_OFFER_TAKER);
|
||||
OfferInfo offer = randomOffer.get();
|
||||
createTakeOfferCliScript(offer);
|
||||
try {
|
||||
log.info("Impatiently waiting for offer {} to be taken, repeatedly calling gettrade.", offer.getId());
|
||||
while (isWithinProtocolStepTimeLimit()) {
|
||||
checkIfShutdownCalled("Interrupted while waiting for offer to be taken.");
|
||||
try {
|
||||
var trade = getNewTrade(offer.getId());
|
||||
if (trade.isPresent())
|
||||
return trade.get();
|
||||
else
|
||||
sleep(randomDelay.get());
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex), ex);
|
||||
}
|
||||
} // end while
|
||||
throw new IllegalStateException("Offer was never taken; we won't wait any longer.");
|
||||
} catch (ManualBotShutdownException ex) {
|
||||
throw ex; // not an error, tells bot to shutdown
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Error while waiting for offer to be taken.", ex);
|
||||
}
|
||||
};
|
||||
|
||||
private Optional<TradeInfo> getNewTrade(String offerId) {
|
||||
try {
|
||||
var trade = botClient.getTrade(offerId);
|
||||
log.info("Offer {} was taken, new trade:\n{}", offerId, TradeFormat.format(trade));
|
||||
return Optional.of(trade);
|
||||
} catch (Exception ex) {
|
||||
// Get trade will throw a non-fatal gRPC exception if not found.
|
||||
log.info(this.getBotClient().toCleanGrpcExceptionMessage(ex));
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void createTakeOfferCliScript(OfferInfo offer) {
|
||||
File script = bashScriptGenerator.createTakeOfferScript(offer);
|
||||
printCliHintAndOrScript(script, "The manual CLI side can take the offer");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package bisq.apitest.scenario.bot.protocol;
|
||||
|
||||
public enum ProtocolStep {
|
||||
START,
|
||||
FIND_OFFER,
|
||||
TAKE_OFFER,
|
||||
WAIT_FOR_OFFER_TAKER,
|
||||
WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED,
|
||||
WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED,
|
||||
SEND_PAYMENT_STARTED_MESSAGE,
|
||||
WAIT_FOR_PAYMENT_STARTED_MESSAGE,
|
||||
SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE,
|
||||
WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE,
|
||||
WAIT_FOR_PAYOUT_TX,
|
||||
KEEP_FUNDS,
|
||||
DONE
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
15
build.gradle
15
build.gradle
|
@ -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'
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)";
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
453
cli/src/main/java/bisq/cli/GrpcClient.java
Normal file
453
cli/src/main/java/bisq/cli/GrpcClient.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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"});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
180
cli/src/test/java/bisq/cli/opt/OptionParsersTest.java
Normal file
180
cli/src/test/java/bisq/cli/opt/OptionParsersTest.java
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -73,6 +73,6 @@ public enum BaseCurrencyNetwork {
|
|||
}
|
||||
|
||||
public long getDefaultMinFeePerVbyte() {
|
||||
return 2;
|
||||
return 15; // 2021-02-22 due to mempool congestion, increased from 2
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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{" +
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue