general rebase in order to update payment methods and desktop app

Co-authored-by: Alva Swanson <alvasw@protonmail.com>
Co-authored-by: andyheko <haoen.ko@gmail.com>
Co-authored-by: Bisq GitHub Admin <51445974+bisq-github-admin-3@users.noreply.github.com>
Co-authored-by: BtcContributor <79100296+BtcContributor@users.noreply.github.com>
Co-authored-by: cd2357 <cd2357@users.noreply.github.com>
Co-authored-by: chimp1984 <chimp1984@gmx.com>
Co-authored-by: Chris Beams <chris@beams.io>
Co-authored-by: Christoph Atteneder <christoph.atteneder@gmail.com>
Co-authored-by: Devin Bileck <603793+devinbileck@users.noreply.github.com>
Co-authored-by: ghubstan <36207203+ghubstan@users.noreply.github.com>
Co-authored-by: Huey <hueydane@gmail.com>
Co-authored-by: Jakub Loucký <jakub.loucky@outlook.cz>
Co-authored-by: jmacxx <47253594+jmacxx@users.noreply.github.com>
Co-authored-by: KanoczTomas <tomas.kanocz@cnl.sk>
Co-authored-by: m52go <735155+m52go@users.noreply.github.com>
Co-authored-by: Marcus0x <marcus0x@xrhodium.org>
Co-authored-by: MarnixCroes <93143998+MarnixCroes@users.noreply.github.com>
Co-authored-by: Martin Harrigan <martinharrigan@gmail.com>
Co-authored-by: MwithM <50149324+MwithM@users.noreply.github.com>
Co-authored-by: sqrrm <sqrrm@users.noreply.github.com>
Co-authored-by: Stan <36207203+ghubstan@users.noreply.github.com>
Co-authored-by: Stephan Oeste <emzy@emzy.de>
Co-authored-by: Steven Barclay <stejbac@gmail.com>
Co-authored-by: WAT <shiido.it@gmail.com>
Co-authored-by: wiz <j@wiz.biz>
Co-authored-by: xyzmaker123 <84982606+xyzmaker123@users.noreply.github.com>
This commit is contained in:
woodser 2022-05-26 13:42:10 -04:00
parent 15a1fe8a36
commit 88578bed10
539 changed files with 27629 additions and 8178 deletions

View file

@ -19,7 +19,7 @@ option adjustments to compensate.
**Java SDK**: Version 10, 11, or 12 **Java SDK**: Version 10, 11, or 12
**Bitcoin-Core**: Version 0.19, 0.20, or 0.21 **Bitcoin-Core**: Version 0.19 - 22
**Git Client** **Git Client**
@ -252,9 +252,9 @@ To remove a custom withdrawal transaction fee rate preference, and revert to the
$ ./bisq-cli --password=xyz unsettxfeerate $ ./bisq-cli --password=xyz unsettxfeerate
``` ```
### Creating Test Payment Accounts ### Creating Test Fiat Payment Accounts
Creating a payment account using the Api involves three steps: Creating a fiat 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 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`): create a face-to-face type payment account, find the face-to-face payment-method-id (`F2F`):
@ -286,6 +286,21 @@ Creating a payment account using the Api involves three steps:
$ ./bisq-cli --password=xyz --port=9998 getpaymentaccts $ ./bisq-cli --password=xyz --port=9998 getpaymentaccts
``` ```
### Creating Test Altcoin Payment Accounts
Unlike more complex fiat payment account setups, the `createcryptopaymentacct` command does not require a json form.
#### XMR Altcoin Payment Accounts
To create an XMR Altcoin payment account associated with example XMR address
`44G4jWmSvTEfifSUZzTDnJVLPvYATmq9XhhtDqUof1BGCLceG82EQsVYG9Q9GN4bJcjbAJEc1JD1m5G7iK4UPZqACubV4Mq`:
```
$ ./bisq-cli --password=xyz --port=9999 createcryptopaymentacct --account-name=XMR-Account \
--currency-code=XMR
--address=44G4jWmSvTEfifSUZzTDnJVLPvYATmq9XhhtDqUof1BGCLceG82EQsVYG9Q9GN4bJcjbAJEc1JD1m5G7iK4UPZqACubV4Mq
```
### Creating Offers ### Creating Offers
The createoffer command is the Api's most complex command (so far), but CLI posix-style options are self-explanatory, The createoffer command is the Api's most complex command (so far), but CLI posix-style options are self-explanatory,
@ -297,31 +312,29 @@ $ ./bisq-cli --password=xyz --port=9998 createoffer --help
#### Examples #### Examples
The `trade-simulation.sh` script described above is an easy way to figure out how to use this command. 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, 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 BTC. Alice had already created an EUR face-to-face payment account with id Alice had already created an EUR face-to-face payment account with id
`f3c1ec8b-9761-458d-b13d-9039c6892413`, and used this `createoffer` command: `f3c1ec8b-9761-458d-b13d-9039c6892413`, and used this `createoffer` command:
``` ```
$ ./bisq-cli --password=xyz --port=9998 createoffer \ $ ./bisq-cli --password=xyz --port=9998 createoffer \
--payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \ --payment-account-id=f3c1ec8b-9761-458d-b13d-9039c6892413 \
--direction=BUY \ --direction=BUY \
--currency-code=EUR \ --currency-code=EUR \
--amount=0.125 \ --amount=0.125 \
--fixed-price=30800 \ --fixed-price=30800 \
--security-deposit=15.0 \ --security-deposit=15.0
--fee-currency=BTC
``` ```
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, 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: putting up a 15% security deposit, the `createoffer` command to do that would be:
``` ```
$ ./bisq-cli --password=xyz --port=9998 createoffer \ $ ./bisq-cli --password=xyz --port=9998 createoffer \
--payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \ --payment-account-id=f3c1ec8b-9761-458d-b13d-9039c6892413 \
--direction=SELL \ --direction=SELL \
--currency-code=JPY \ --currency-code=JPY \
--amount=0.125 \ --amount=0.125 \
--market-price-margin=0.5 \ --market-price-margin=0.5 \
--security-deposit=15.0 \ --security-deposit=15.0
--fee-currency=BTC
``` ```
The `trade-simulation.sh` script options that would generate the previous `createoffer` example is: The `trade-simulation.sh` script options that would generate the previous `createoffer` example is:
@ -340,7 +353,7 @@ $ ./bisq-cli --password=xyz --port=9998 getmyoffers --direction=<BUY|SELL> --cur
To look at a specific offer you created: To look at a specific offer you created:
``` ```
$ ./bisq-cli --password=xyz --port=9998 getmyoffer --offer-id=<offer-id> $ ./bisq-cli --password=xyz --port=9998 getoffer --offer-id=<offer-id>
``` ```
### Browsing Available Offers ### Browsing Available Offers
@ -365,8 +378,116 @@ The offer will be removed from other Bisq users' offer views, and paid transacti
### Editing an Existing Offer ### Editing an Existing Offer
Editing existing offers is not yet supported. You can cancel and re-create an offer, but paid transaction fees Offers you create can be edited in various ways:
for the canceled offer will be forfeited.
- Disable or re-enable an offer.
- Change an offer's price model and disable (or re-enable) it.
- Change a market price margin based offer to a fixed price offer.
- Change a market price margin based offer's price margin.
- Change, set, or remove a trigger price on a market price margin based offer.
- Change a market price margin based offer's price margin and trigger price.
- Change a market price margin based offer's price margin and remove its trigger price.
- Change a fixed price offer to a market price margin based offer.
- Change a fixed price offer's fixed price.
_Note: the API does not support editing an offer's payment account._
The subsections below contain examples related to specific use cases.
#### Enable and Disable Offer
Existing offers you create can be disabled (removed from offer book) and re-enabled (re-published to offer book).
To disable an offer:
```
./bisq-cli --password=xyz --port=9998 editoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--enable=false
```
To enable an offer:
```
./bisq-cli --password=xyz --port=9998 editoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--enable=true
```
#### Change Offer Pricing Model
The `editoffer` command can be used to change an existing market price margin based offer to a fixed price offer,
and vice-versa.
##### Change Market Price Margin Based to Fixed Price Offer
Suppose you used `createoffer` to create a market price margin based offer as follows:
```
$ ./bisq-cli --password=xyz --port=9998 createoffer \
--payment-account-id=f3c1ec8b-9761-458d-b13d-9039c6892413 \
--direction=SELL \
--currency-code=JPY \
--amount=0.125 \
--market-price-margin=0.5 \
--security-deposit=15.0
```
To change the market price margin based offer to a fixed price offer:
```
./bisq-cli --password=xyz --port=9998 editoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--fixed-price=3960000.5555
```
##### Change Fixed Price Offer to Market Price Margin Based Offer
Suppose you used `createoffer` to create a fixed price offer as follows:
```
$ ./bisq-cli --password=xyz --port=9998 createoffer \
--payment-account-id=f3c1ec8b-9761-458d-b13d-9039c6892413 \
--direction=SELL \
--currency-code=JPY \
--amount=0.125 \
--fixed-price=3960000.0000 \
--security-deposit=15.0
```
To change the fixed price offer to a market price margin based offer:
```
./bisq-cli --password=xyz --port=9998 editoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--market-price-margin=0.5
```
Alternatively, you can also set a trigger price on the re-published, market price margin based offer.
A trigger price on a SELL offer causes the offer to be automatically disabled when the market price
falls below the trigger price. In the `editoffer` example below, the SELL offer will be disabled when
the JPY market price falls below 3960000.0000.
```
./bisq-cli --password=xyz --port=9998 editoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--market-price-margin=0.5 \
--trigger-price=3960000.0000
```
On a BUY offer, a trigger price causes the BUY offer to be automatically disabled when the market price
rises above the trigger price.
_Note: Disabled offers never automatically re-enable; they can only be manually re-enabled via
`editoffer --offer-id=<id> --enable=true`._
#### Remove Trigger Price
To remove a trigger price on a market price margin based offer, set the trigger price to 0:
```
./bisq-cli --password=xyz --port=9998 editoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--trigger-price=0
```
#### Change Disabled Offer's Pricing Model and Enable It
You can use `editoffer` to simultaneously change an offer's price details and disable or re-enable it.
Suppose you have a disabled, fixed price offer, and want to change it to a market price margin based offer, set
a trigger price, and re-enable it:
```
./bisq-cli --password=xyz --port=9998 editoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--market-price-margin=0.5 \
--trigger-price=3960000.0000 \
--enable=true
```
### Taking Offers ### Taking Offers
@ -377,16 +498,14 @@ A CLI user browses available offers with the getoffers command. For example, th
$ ./bisq-cli --password=xyz --port=9998 getoffers --direction=SELL --currency-code=EUR $ ./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`) Then takes one of the available offers with an EUR payment account ( id `fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e`)
with the `takeoffer` command: with the `takeoffer` command:
``` ```
$ ./bisq-cli --password=xyz --port=9998 takeoffer \ $ ./bisq-cli --password=xyz --port=9998 takeoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--payment-account=fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e \ --payment-account-id=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 next section describes how to use the Api to execute a trade.
the trade.
### Completing Trade Protocol ### Completing Trade Protocol
@ -429,19 +548,19 @@ protocol completed. There are three CLI commands that must be performed in coor
``` ```
confirmpaymentstarted Buyer sends seller a message confirming payment has been sent. confirmpaymentstarted Buyer sends seller a message confirming payment has been sent.
confirmpaymentreceived Seller sends buyer a message confirming payment has been received. confirmpaymentreceived Seller sends buyer a message confirming payment has been received.
keepfunds Keep trade proceeds in their Bisq wallets. closetrade Set trade state to CLOSED, and keep trade proceeds in user's Bisq wallet.
OR OR
withdrawfunds Send trade proceeds to an external wallet. withdrawfunds Set trade state to CLOSED, and send trade proceeds to an external wallet.
``` ```
The last two mutually exclusive commands (`keepfunds` or `withdrawfunds`) may seem unnecessary, but they are critical The last two mutually exclusive commands (`closetrade` or `withdrawfunds`) may seem unnecessary, but they are critical
because they inform the Bisq node that a trades state can be set to `CLOSED`. Please close out your trades with one because they tell the Bisq node to set a completed trades state `CLOSED`. Please close out your trades with one
or the other command. or the other command.
Each of the CLI commands above takes one argument: `--trade-id=<trade-id>`: 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=9998 confirmpaymentstarted --trade-id=<trade-id>
$ ./bisq-cli --password=xyz --port=9999 confirmpaymentreceived --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=9998 closetrade --trade-id=<trade-id>
$ ./bisq-cli --password=xyz --port=9999 withdrawfunds --trade-id=<trade-id> --address=<btc-address> [--memo=<"memo">] $ ./bisq-cli --password=xyz --port=9999 withdrawfunds --trade-id=<trade-id> --address=<btc-address> [--memo=<"memo">]
``` ```

View file

@ -4,7 +4,7 @@ The Java based API runs on Linux and OSX.
## Mainnet ## Mainnet
To build from the source, clone the github repository found at `https://github.com/bisq-network/bisq`, To build from the source, clone the GitHub repository found at `https://github.com/bisq-network/bisq`,
and build with gradle: and build with gradle:
$ ./gradlew clean build $ ./gradlew clean build

View file

@ -6,7 +6,7 @@
# #
# Prerequisites: # Prerequisites:
# #
# - Linux or OSX with bash, Java 10, or Java 11-12 (JDK language compatibility 10), and bitcoin-core (v0.19, v0.20, v0.21). # - Linux or OSX with bash, Java 11-15 (JDK language compatibility 11), and bitcoin-core (v0.19 - v22).
# #
# - Bisq must be fully built with apitest dao setup files installed. # - Bisq must be fully built with apitest dao setup files installed.
# Build command: `./gradlew clean build :apitest:installDaoSetup` # Build command: `./gradlew clean build :apitest:installDaoSetup`
@ -134,7 +134,7 @@ sleeptraced 3
# Show Alice's new offer. # Show Alice's new offer.
printdate "ALICE: Looking at her new $DIRECTION $CURRENCY_CODE offer." printdate "ALICE: Looking at her new $DIRECTION $CURRENCY_CODE offer."
CMD="$CLI_BASE --port=$ALICE_PORT getmyoffer --offer-id=$OFFER_ID" CMD="$CLI_BASE --port=$ALICE_PORT getoffer --offer-id=$OFFER_ID"
printdate "ALICE CLI: $CMD" printdate "ALICE CLI: $CMD"
OFFER=$($CMD) OFFER=$($CMD)
exitoncommandalert $? exitoncommandalert $?

View file

@ -10,7 +10,7 @@
# #
# Prerequisites: # Prerequisites:
# #
# - Linux or OSX with bash, Java 10, or Java 11-12 (JDK language compatibility 10), and bitcoin-core (v0.19, v0.20, v0.21). # - Linux or OSX with bash, Java 11-15 (JDK language compatibility 11), and bitcoin-core (v0.19 - v22).
# #
# - Bisq must be fully built with apitest dao setup files installed. # - Bisq must be fully built with apitest dao setup files installed.
# Build command: `./gradlew clean build :apitest:installDaoSetup` # Build command: `./gradlew clean build :apitest:installDaoSetup`
@ -94,7 +94,7 @@ while : ; do
# Show Alice's new offer. # Show Alice's new offer.
printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer." printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer."
CMD="$CLI_BASE --port=$ALICE_PORT getmyoffer --offer-id=$OFFER_ID" CMD="$CLI_BASE --port=$ALICE_PORT getoffer --offer-id=$OFFER_ID"
printdate "ALICE CLI: $CMD" printdate "ALICE CLI: $CMD"
OFFER=$($CMD) OFFER=$($CMD)
exitoncommandalert $? exitoncommandalert $?

View file

@ -193,7 +193,6 @@ gencreateoffercommand() {
CMD+=" --market-price-margin=$MKT_PRICE_MARGIN" CMD+=" --market-price-margin=$MKT_PRICE_MARGIN"
fi fi
CMD+=" --security-deposit=15.0" CMD+=" --security-deposit=15.0"
CMD+=" --fee-currency=BTC"
echo "$CMD" echo "$CMD"
} }
@ -368,7 +367,7 @@ waitfortradepaymentsent() {
IS_TRADE_PAYMENT_SENT=$(istradepaymentsent "$TRADE_DETAIL") IS_TRADE_PAYMENT_SENT=$(istradepaymentsent "$TRADE_DETAIL")
exitoncommandalert $? exitoncommandalert $?
printdate "$SELLER: Has buyer's fiat payment been initiated? $IS_TRADE_PAYMENT_SENT" printdate "$SELLER: Has buyer's payment been initiated? $IS_TRADE_PAYMENT_SENT"
if [ "$IS_TRADE_PAYMENT_SENT" = "YES" ] if [ "$IS_TRADE_PAYMENT_SENT" = "YES" ]
then then
DONE=1 DONE=1
@ -407,7 +406,7 @@ waitfortradepaymentreceived() {
# but we do not need to simulate that in this regtest script. # but we do not need to simulate that in this regtest script.
IS_TRADE_PAYMENT_SENT=$(istradepaymentreceived "$TRADE_DETAIL") IS_TRADE_PAYMENT_SENT=$(istradepaymentreceived "$TRADE_DETAIL")
exitoncommandalert $? exitoncommandalert $?
printdate "$SELLER: Has buyer's payment been transferred to seller's fiat account? $IS_TRADE_PAYMENT_SENT" printdate "$SELLER: Has buyer's payment been transferred to seller's account? $IS_TRADE_PAYMENT_SENT"
if [ "$IS_TRADE_PAYMENT_SENT" = "YES" ] if [ "$IS_TRADE_PAYMENT_SENT" = "YES" ]
then then
DONE=1 DONE=1
@ -427,7 +426,7 @@ delayconfirmpaymentstarted() {
PORT="$2" PORT="$2"
OFFER_ID="$3" OFFER_ID="$3"
RANDOM_WAIT=$(echo $[$RANDOM % 5 + 1]) RANDOM_WAIT=$(echo $[$RANDOM % 5 + 1])
printdate "$PAYER: Sending fiat payment sent message to seller in $RANDOM_WAIT seconds..." printdate "$PAYER: Sending 'payment sent' message to seller in $RANDOM_WAIT seconds..."
sleeptraced "$RANDOM_WAIT" sleeptraced "$RANDOM_WAIT"
CMD="$CLI_BASE --port=$PORT confirmpaymentstarted --trade-id=$OFFER_ID" CMD="$CLI_BASE --port=$PORT confirmpaymentstarted --trade-id=$OFFER_ID"
printdate "$PAYER_CLI: $CMD" printdate "$PAYER_CLI: $CMD"
@ -446,7 +445,7 @@ delayconfirmpaymentreceived() {
PORT="$2" PORT="$2"
OFFER_ID="$3" OFFER_ID="$3"
RANDOM_WAIT=$(echo $[$RANDOM % 5 + 1]) RANDOM_WAIT=$(echo $[$RANDOM % 5 + 1])
printdate "$PAYEE: Sending fiat payment sent message to seller in $RANDOM_WAIT seconds..." printdate "$PAYEE: Sending 'payment sent' message to seller in $RANDOM_WAIT seconds..."
sleeptraced "$RANDOM_WAIT" sleeptraced "$RANDOM_WAIT"
CMD="$CLI_BASE --port=$PORT confirmpaymentreceived --trade-id=$OFFER_ID" CMD="$CLI_BASE --port=$PORT confirmpaymentreceived --trade-id=$OFFER_ID"
printdate "$PAYEE_CLI: $CMD" printdate "$PAYEE_CLI: $CMD"
@ -457,11 +456,10 @@ delayconfirmpaymentreceived() {
printbreak printbreak
} }
# This is a large function that should be broken up if it ever makes sense to not treat a trade # This is a large function that might be split into smaller functions. But we are not testing
# execution simulation as an atomic operation. But we are not testing api methods here, just # api methods here, just demonstrating how to use them to get through the V1 trade protocol with
# demonstrating how to use them to get through the trade protocol. It should work for any trade # the CLI. It should work for any trade between Bob & Alice, as long as Alice is maker, Bob is
# between Bob & Alice, as long as Alice is maker, Bob is taker, and the offer to be taken is the # taker, and the offer to be taken is the first displayed in Bob's getoffers command output.
# first displayed in Bob's getoffers command output.
executetrade() { executetrade() {
# Bob list available offers. # Bob list available offers.
printdate "BOB $BOB_ROLE: Looking at $DIRECTION $CURRENCY_CODE offers." printdate "BOB $BOB_ROLE: Looking at $DIRECTION $CURRENCY_CODE offers."
@ -532,24 +530,27 @@ executetrade() {
fi fi
# Generate some btc blocks # Generate some btc blocks
printdate "Generating btc blocks after fiat transfer." printdate "Generating btc blocks after payment."
genbtcblocks 2 2 genbtcblocks 2 2
printbreak printbreak
# Complete the trade on the seller side. # Complete the trade on both sides
if [ "$DIRECTION" = "BUY" ] printdate "BOB $BOB_ROLE: Closing trade and keeping funds in Bisq wallet."
then CMD="$CLI_BASE --port=$BOB_PORT closetrade --trade-id=$OFFER_ID"
printdate "BOB $BOB_ROLE: Closing trade by keeping funds in Bisq wallet." printdate "BOB CLI: $CMD"
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) KEEP_FUNDS_MSG=$($CMD)
commandalert $? "Could close trade with keepfunds command." commandalert $? "Closed trade with closetrade command."
# Print the keepfunds command's console output. # Print the closetrade command's console output.
printdate "$KEEP_FUNDS_MSG"
sleeptraced 3
printbreak
printdate "ALICE (taker): Closing trade and keeping funds in Bisq wallet."
CMD="$CLI_BASE --port=$ALICE_PORT closetrade --trade-id=$OFFER_ID"
printdate "ALICE CLI: $CMD"
KEEP_FUNDS_MSG=$($CMD)
commandalert $? "Closed trade with closetrade command."
# Print the closetrade command's console output.
printdate "$KEEP_FUNDS_MSG" printdate "$KEEP_FUNDS_MSG"
sleeptraced 3 sleeptraced 3
printbreak printbreak

View file

@ -1,13 +1,13 @@
#! /bin/bash #! /bin/bash
# Runs fiat <-> btc trading scenarios using the API CLI with a local regtest bitcoin node. # Demonstrates a fiat <-> btc trade 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 # 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. # trade, and the maker's face to face payment account's currency code is used when creating the offer.
# #
# Prerequisites: # Prerequisites:
# #
# - Linux or OSX with bash, Java 10, or Java 11-12 (JDK language compatibility 10), and bitcoin-core (v0.19, v0.20, or v0.21). # - Linux or OSX with bash, Java 11-15 (JDK language compatibility 11), and bitcoin-core (v0.19 - v22).
# #
# - Bisq must be fully built with apitest dao setup files installed. # - Bisq must be fully built with apitest dao setup files installed.
# Build command: `./gradlew clean build :apitest:installDaoSetup` # Build command: `./gradlew clean build :apitest:installDaoSetup`
@ -26,15 +26,16 @@
# #
# `$ apitest/scripts/trade-simulation.sh -d buy -c fr -m 3.00 -a 0.125` # `$ 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)> # Script options: -d <direction> -c <country-code> -m <mkt-price-margin(%)> -f <fixed-price> -a <amount(btc)>
# #
# Examples: # 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: # Create and take 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` # `$ 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 # Create and take 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: # payment account:
# #
# `$ apitest/scripts/trade-simulation.sh -d sell -c fr -f 38000 -a 0.125` # `$ apitest/scripts/trade-simulation.sh -d sell -c fr -f 38000 -a 0.125`
@ -53,8 +54,6 @@ printdate "Started $APP_BASE_NAME with parameters:"
printscriptparams printscriptparams
printbreak printbreak
registerdisputeagents
# Demonstrate how to create a country based, face to face account. # Demonstrate how to create a country based, face to face account.
showcreatepaymentacctsteps "Alice" "$ALICE_PORT" showcreatepaymentacctsteps "Alice" "$ALICE_PORT"
@ -96,7 +95,7 @@ sleeptraced 3
# Show Alice's new offer. # Show Alice's new offer.
printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer." printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer."
CMD="$CLI_BASE --port=$ALICE_PORT getmyoffer --offer-id=$OFFER_ID" CMD="$CLI_BASE --port=$ALICE_PORT getoffer --offer-id=$OFFER_ID"
printdate "ALICE CLI: $CMD" printdate "ALICE CLI: $CMD"
OFFER=$($CMD) OFFER=$($CMD)
exitoncommandalert $? exitoncommandalert $?

View file

@ -0,0 +1,122 @@
#! /bin/bash
# Runs xmr <-> btc trading scenarios using the API CLI with a local regtest bitcoin node.
#
# Prerequisites:
#
# - Linux or OSX with bash, Java 11-15 (JDK language compatibility 11), and bitcoin-core (v0.19 - v22).
#
# - 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`
#
# Usage:
#
# This script must be run from the root of the project, e.g.:
#
# `$ apitest/scripts/trade-xmr-simulation.sh -d buy -f 0.05 -a 0.125`
#
# Script options: -d <direction> -m <mkt-price-margin(%)> -f <fixed-price> -a <amount(btc)>
#
# Examples:
#
# Create a buy/xmr offer to buy 0.125 btc at an xmr fixed-price of 0.05 btc, using an xmr payment account:
#
# `$ apitest/scripts/trade-xmr-simulation.sh -d buy -f 0.05 -a 0.125`
#
# Create a sell/xmr offer to sell 0.125 btc at at an xmr mkt-price-margin of 0%, using using an xmr payment account:
#
# `$ apitest/scripts/trade-xmr-simulation.sh -d sell -m 0.00 -a 0.125`
export APP_BASE_NAME=$(basename "$0")
export APP_HOME=$(pwd -P)
export APITEST_SCRIPTS_HOME="$APP_HOME/apitest/scripts"
export CURRENCY_CODE="XMR"
export ALICE_XMR_ADDRESS="44i8xZbd8ecaD6nQQrHjr1BwTp6QfGL22iWqHZKmU4QYSyr1F64XAxM4HgvQHxbny7ehfxemaA9LPDLz2wY3fxhB1bbMEco"
export BOB_XMR_ADDRESS="48xdBkXaCosPxcWwXRZdSGc33M9tYu6k9ga56dqkNrgsjQuJX16xW2qTyWTZstJpXXj87dj5p4H3y1xAfoVjAysoAYrXh2N"
source "$APITEST_SCRIPTS_HOME/trade-simulation-env.sh"
source "$APITEST_SCRIPTS_HOME/trade-simulation-utils.sh"
checksetup
parsexmrscriptopts "$@"
printdate "Started $APP_BASE_NAME with parameters:"
printscriptparams
printbreak
registerdisputeagents
# Demonstrate how to create an XMR altcoin payment account.
printdate "Create Alice's XMR Trading Payment Account."
# Note: Having problems passing a double quoted --account-name param to function.
CMD="$CLI_BASE --port=$ALICE_PORT createcryptopaymentacct --account-name=Alice_XMR_Account"
CMD+=" --currency-code=XMR --address=$ALICE_XMR_ADDRESS --trade-instant=false"
printdate "ALICE CLI: $CMD"
CMD_OUTPUT=$(createpaymentacct "$CMD")
echo "$CMD_OUTPUT"
printbreak
export ALICE_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT")
printdate "Alice's XMR payment-account-id: $ALICE_ACCT_ID"
exitoncommandalert $?
printbreak
printdate "Create Bob's XMR Trading Payment Account."
# Note: Having problems passing a double quoted --account-name param to function.
CMD="$CLI_BASE --port=$BOB_PORT createcryptopaymentacct --account-name=Bob_XMR_Account"
CMD+=" --currency-code=XMR --address=$BOB_XMR_ADDRESS --trade-instant=false"
printdate "BOB CLI: $CMD"
CMD_OUTPUT=$(createpaymentacct "$CMD")
echo "$CMD_OUTPUT"
printbreak
export BOB_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT")
printdate "Bob's XMR payment-account-id: $BOB_ACCT_ID"
exitoncommandalert $?
printbreak
# Alice creates an offer.
printdate "ALICE $ALICE_ROLE: Creating $DIRECTION $CURRENCY_CODE offer with payment acct $ALICE_ACCT_ID."
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 getoffer --offer-id=$OFFER_ID"
printdate "ALICE CLI: $CMD"
OFFER=$($CMD)
exitoncommandalert $?
echo "$OFFER"
printbreak
sleeptraced 3
# Generate some btc blocks.
printdate "Generating btc blocks after publishing Alice's offer."
genbtcblocks 3 1
printbreak
# Go through the trade protocol.
executetrade
exitoncommandalert $?
printbreak
# Get balances after trade completion.
printdate "Bob & Alice's balances after trade:"
printdate "ALICE CLI:"
printbalances "$ALICE_PORT"
printbreak
printdate "BOB CLI:"
printbalances "$BOB_PORT"
printbreak
exit 0

View file

@ -17,10 +17,15 @@
package bisq.apitest; package bisq.apitest;
import java.io.File;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.Scaffold.EXIT_FAILURE; import static bisq.apitest.Scaffold.EXIT_FAILURE;
import static bisq.apitest.Scaffold.EXIT_SUCCESS; import static bisq.apitest.Scaffold.EXIT_SUCCESS;
import static bisq.apitest.config.ApiTestRateMeterInterceptorConfig.appendCallRateMeteringConfigPathOpt;
import static bisq.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig;
import static bisq.apitest.config.ApiTestRateMeterInterceptorConfig.hasCallRateMeteringConfigPathOpt;
import static java.lang.System.err; import static java.lang.System.err;
import static java.lang.System.exit; import static java.lang.System.exit;
@ -32,7 +37,7 @@ import bisq.apitest.config.ApiTestConfig;
* ApiTestMain is a placeholder for the gradle build file, which requires a valid * ApiTestMain is a placeholder for the gradle build file, which requires a valid
* 'mainClassName' property in the :apitest subproject configuration. * 'mainClassName' property in the :apitest subproject configuration.
* *
* It does has some uses: * It has some uses:
* *
* It can be used to print test scaffolding options: bisq-apitest --help. * It can be used to print test scaffolding options: bisq-apitest --help.
* *
@ -41,19 +46,23 @@ import bisq.apitest.config.ApiTestConfig;
* It can be used to run the regtest environment for release testing: * It can be used to run the regtest environment for release testing:
* bisq-test --shutdownAfterTests=false * bisq-test --shutdownAfterTests=false
* *
* All method, scenario and end to end tests are found in the test sources folder. * All method, scenario and end-to-end tests are found in the test sources folder.
* *
* Requires bitcoind v0.19, v0.20, or v0.21. * Requires bitcoind v0.19 - v22.
*/ */
@Slf4j @Slf4j
public class ApiTestMain { public class ApiTestMain {
public static void main(String[] args) { public static void main(String[] args) {
new ApiTestMain().execute(args); if (!hasCallRateMeteringConfigPathOpt(args))
new ApiTestMain().execute(getAppendedArgs(args));
else
new ApiTestMain().execute(args);
} }
public void execute(@SuppressWarnings("unused") String[] args) { public void execute(String[] args) {
try { try {
log.info("Configuring test harness with options:\n\t{}", String.join("\n\t", args));
Scaffold scaffold = new Scaffold(args).setUp(); Scaffold scaffold = new Scaffold(args).setUp();
ApiTestConfig config = scaffold.config; ApiTestConfig config = scaffold.config;
@ -77,4 +86,9 @@ public class ApiTestMain {
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
} }
private static String[] getAppendedArgs(String[] args) {
File rateMeterInterceptorConfig = getTestRateMeterInterceptorConfig();
return appendCallRateMeteringConfigPathOpt(args, rateMeterInterceptorConfig);
}
} }

View file

@ -160,6 +160,7 @@ public class Scaffold {
try { try {
log.info("Shutting down executor service ..."); log.info("Shutting down executor service ...");
executor.shutdownNow(); executor.shutdownNow();
//noinspection ResultOfMethodCallIgnored
executor.awaitTermination(config.supportingApps.size() * 2000L, MILLISECONDS); executor.awaitTermination(config.supportingApps.size() * 2000L, MILLISECONDS);
SetupTask[] orderedTasks = new SetupTask[]{ SetupTask[] orderedTasks = new SetupTask[]{
@ -189,7 +190,7 @@ public class Scaffold {
MILLISECONDS.sleep(1000); MILLISECONDS.sleep(1000);
if (p.hasShutdownExceptions()) { if (p.hasShutdownExceptions()) {
// We log shutdown exceptions, but do not throw any from here // We log shutdown exceptions, but do not throw any from here
// because all of the background instances must be shut down. // because all the background instances must be shut down.
p.logExceptions(p.getShutdownExceptions(), log); p.logExceptions(p.getShutdownExceptions(), log);
// We cache only the 1st shutdown exception and move on to the // We cache only the 1st shutdown exception and move on to the
@ -221,6 +222,9 @@ public class Scaffold {
} }
private void installCallRateMeteringConfiguration(String dataDir) throws IOException, InterruptedException { private void installCallRateMeteringConfiguration(String dataDir) throws IOException, InterruptedException {
if (config.callRateMeteringConfigPath.isEmpty())
return;
File testRateMeteringFile = new File(config.callRateMeteringConfigPath); File testRateMeteringFile = new File(config.callRateMeteringConfigPath);
if (!testRateMeteringFile.exists()) if (!testRateMeteringFile.exists())
throw new FileNotFoundException( throw new FileNotFoundException(
@ -289,49 +293,49 @@ public class Scaffold {
startBisqApp(bobdesktop, executor, countdownLatch); startBisqApp(bobdesktop, executor, countdownLatch);
} }
private void startBisqApp(HavenoAppConfig havenoAppConfig, private void startBisqApp(HavenoAppConfig HavenoAppConfig,
ExecutorService executor, ExecutorService executor,
CountDownLatch countdownLatch) CountDownLatch countdownLatch)
throws IOException, InterruptedException { throws IOException, InterruptedException {
HavenoProcess bisqProcess = createBisqProcess(havenoAppConfig); HavenoProcess HavenoProcess = createHavenoProcess(HavenoAppConfig);
switch (havenoAppConfig) { switch (HavenoAppConfig) {
case seednode: case seednode:
seedNodeTask = new SetupTask(bisqProcess, countdownLatch); seedNodeTask = new SetupTask(HavenoProcess, countdownLatch);
seedNodeTaskFuture = executor.submit(seedNodeTask); seedNodeTaskFuture = executor.submit(seedNodeTask);
break; break;
case arbdaemon: case arbdaemon:
case arbdesktop: case arbdesktop:
arbNodeTask = new SetupTask(bisqProcess, countdownLatch); arbNodeTask = new SetupTask(HavenoProcess, countdownLatch);
arbNodeTaskFuture = executor.submit(arbNodeTask); arbNodeTaskFuture = executor.submit(arbNodeTask);
break; break;
case alicedaemon: case alicedaemon:
case alicedesktop: case alicedesktop:
aliceNodeTask = new SetupTask(bisqProcess, countdownLatch); aliceNodeTask = new SetupTask(HavenoProcess, countdownLatch);
aliceNodeTaskFuture = executor.submit(aliceNodeTask); aliceNodeTaskFuture = executor.submit(aliceNodeTask);
break; break;
case bobdaemon: case bobdaemon:
case bobdesktop: case bobdesktop:
bobNodeTask = new SetupTask(bisqProcess, countdownLatch); bobNodeTask = new SetupTask(HavenoProcess, countdownLatch);
bobNodeTaskFuture = executor.submit(bobNodeTask); bobNodeTaskFuture = executor.submit(bobNodeTask);
break; break;
default: default:
throw new IllegalStateException("Unknown HavenoAppConfig " + havenoAppConfig.name()); throw new IllegalStateException("Unknown HavenoAppConfig " + HavenoAppConfig.name());
} }
log.info("Giving {} ms for {} to initialize ...", config.bisqAppInitTime, havenoAppConfig.appName); log.info("Giving {} ms for {} to initialize ...", config.bisqAppInitTime, HavenoAppConfig.appName);
MILLISECONDS.sleep(config.bisqAppInitTime); MILLISECONDS.sleep(config.bisqAppInitTime);
if (bisqProcess.hasStartupExceptions()) { if (HavenoProcess.hasStartupExceptions()) {
bisqProcess.logExceptions(bisqProcess.getStartupExceptions(), log); HavenoProcess.logExceptions(HavenoProcess.getStartupExceptions(), log);
throw new IllegalStateException(bisqProcess.getStartupExceptions().get(0)); throw new IllegalStateException(HavenoProcess.getStartupExceptions().get(0));
} }
} }
private HavenoProcess createBisqProcess(HavenoAppConfig havenoAppConfig) private HavenoProcess createHavenoProcess(HavenoAppConfig HavenoAppConfig)
throws IOException, InterruptedException { throws IOException, InterruptedException {
HavenoProcess bisqProcess = new HavenoProcess(havenoAppConfig, config); HavenoProcess HavenoProcess = new HavenoProcess(HavenoAppConfig, config);
bisqProcess.verifyAppNotRunning(); HavenoProcess.verifyAppNotRunning();
bisqProcess.verifyAppDataDirInstalled(); HavenoProcess.verifyAppDataDirInstalled();
return bisqProcess; return HavenoProcess;
} }
private void verifyStartupCompleted() private void verifyStartupCompleted()

View file

@ -56,6 +56,8 @@ public class ApiTestConfig {
// Global constants // Global constants
public static final String BTC = "BTC"; public static final String BTC = "BTC";
public static final String EUR = "EUR";
public static final String USD = "USD";
public static final String XMR = "XMR"; public static final String XMR = "XMR";
public static final String ARBITRATOR = "arbitrator"; public static final String ARBITRATOR = "arbitrator";
public static final String MEDIATOR = "mediator"; public static final String MEDIATOR = "mediator";
@ -149,7 +151,7 @@ public class ApiTestConfig {
ArgumentAcceptingOptionSpec<String> configFileOpt = ArgumentAcceptingOptionSpec<String> configFileOpt =
parser.accepts(CONFIG_FILE, format("Specify configuration file. " + parser.accepts(CONFIG_FILE, format("Specify configuration file. " +
"Relative paths will be prefixed by %s location.", userDir)) "Relative paths will be prefixed by %s location.", userDir))
.withRequiredArg() .withRequiredArg()
.ofType(String.class) .ofType(String.class)
.defaultsTo(DEFAULT_CONFIG_FILE_NAME); .defaultsTo(DEFAULT_CONFIG_FILE_NAME);
@ -206,55 +208,55 @@ public class ApiTestConfig {
ArgumentAcceptingOptionSpec<Boolean> runSubprojectJarsOpt = ArgumentAcceptingOptionSpec<Boolean> runSubprojectJarsOpt =
parser.accepts(RUN_SUBPROJECT_JARS, parser.accepts(RUN_SUBPROJECT_JARS,
"Run subproject build jars instead of full build jars") "Run subproject build jars instead of full build jars")
.withRequiredArg() .withRequiredArg()
.ofType(Boolean.class) .ofType(Boolean.class)
.defaultsTo(false); .defaultsTo(false);
ArgumentAcceptingOptionSpec<Long> bisqAppInitTimeOpt = ArgumentAcceptingOptionSpec<Long> bisqAppInitTimeOpt =
parser.accepts(BISQ_APP_INIT_TIME, parser.accepts(BISQ_APP_INIT_TIME,
"Amount of time (ms) to wait on a Bisq instance's initialization") "Amount of time (ms) to wait on a Bisq instance's initialization")
.withRequiredArg() .withRequiredArg()
.ofType(Long.class) .ofType(Long.class)
.defaultsTo(5000L); .defaultsTo(5000L);
ArgumentAcceptingOptionSpec<Boolean> skipTestsOpt = ArgumentAcceptingOptionSpec<Boolean> skipTestsOpt =
parser.accepts(SKIP_TESTS, parser.accepts(SKIP_TESTS,
"Start apps, but skip tests") "Start apps, but skip tests")
.withRequiredArg() .withRequiredArg()
.ofType(Boolean.class) .ofType(Boolean.class)
.defaultsTo(false); .defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> shutdownAfterTestsOpt = ArgumentAcceptingOptionSpec<Boolean> shutdownAfterTestsOpt =
parser.accepts(SHUTDOWN_AFTER_TESTS, parser.accepts(SHUTDOWN_AFTER_TESTS,
"Terminate all processes after tests") "Terminate all processes after tests")
.withRequiredArg() .withRequiredArg()
.ofType(Boolean.class) .ofType(Boolean.class)
.defaultsTo(true); .defaultsTo(true);
ArgumentAcceptingOptionSpec<String> supportingAppsOpt = ArgumentAcceptingOptionSpec<String> supportingAppsOpt =
parser.accepts(SUPPORTING_APPS, parser.accepts(SUPPORTING_APPS,
"Comma delimited list of supporting apps (bitcoind,seednode,arbdaemon,...") "Comma delimited list of supporting apps (bitcoind,seednode,arbdaemon,...")
.withRequiredArg() .withRequiredArg()
.ofType(String.class) .ofType(String.class)
.defaultsTo("bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon"); .defaultsTo("bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon");
ArgumentAcceptingOptionSpec<String> callRateMeteringConfigPathOpt = ArgumentAcceptingOptionSpec<String> callRateMeteringConfigPathOpt =
parser.accepts(CALL_RATE_METERING_CONFIG_PATH, parser.accepts(CALL_RATE_METERING_CONFIG_PATH,
"Install a ratemeters.json file to configure call rate metering interceptors") "Install a ratemeters.json file to configure call rate metering interceptors")
.withRequiredArg() .withRequiredArg()
.defaultsTo(EMPTY); .defaultsTo(EMPTY);
ArgumentAcceptingOptionSpec<Boolean> enableBisqDebuggingOpt = ArgumentAcceptingOptionSpec<Boolean> enableBisqDebuggingOpt =
parser.accepts(ENABLE_BISQ_DEBUGGING, parser.accepts(ENABLE_BISQ_DEBUGGING,
"Start Bisq apps with remote debug options") "Start Bisq apps with remote debug options")
.withRequiredArg() .withRequiredArg()
.ofType(Boolean.class) .ofType(Boolean.class)
.defaultsTo(false); .defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> registerDisputeAgentsOpt = ArgumentAcceptingOptionSpec<Boolean> registerDisputeAgentsOpt =
parser.accepts(REGISTER_DISPUTE_AGENTS, parser.accepts(REGISTER_DISPUTE_AGENTS,
"Register dispute agents in arbitration daemon") "Register dispute agents in arbitration daemon")
.withRequiredArg() .withRequiredArg()
.ofType(Boolean.class) .ofType(Boolean.class)
.defaultsTo(true); .defaultsTo(true);

View file

@ -0,0 +1,70 @@
package bisq.apitest.config;
import java.io.File;
import static bisq.apitest.config.ApiTestConfig.CALL_RATE_METERING_CONFIG_PATH;
import static bisq.proto.grpc.DisputeAgentsGrpc.getRegisterDisputeAgentMethod;
import static bisq.proto.grpc.GetVersionGrpc.getGetVersionMethod;
import static java.lang.System.arraycopy;
import static java.util.Arrays.stream;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.daemon.grpc.GrpcVersionService;
import bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig;
public class ApiTestRateMeterInterceptorConfig {
public static File getTestRateMeterInterceptorConfig() {
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;
}
public static boolean hasCallRateMeteringConfigPathOpt(String[] args) {
return stream(args).anyMatch(a -> a.contains("--" + CALL_RATE_METERING_CONFIG_PATH));
}
public static String[] appendCallRateMeteringConfigPathOpt(String[] args, File rateMeterInterceptorConfig) {
String[] rateMeteringConfigPathOpt = new String[]{
"--" + CALL_RATE_METERING_CONFIG_PATH + "=" + rateMeterInterceptorConfig.getAbsolutePath()
};
if (args.length == 0) {
return rateMeteringConfigPathOpt;
} else {
String[] appendedOpts = new String[args.length + 1];
arraycopy(args, 0, appendedOpts, 0, args.length);
arraycopy(rateMeteringConfigPathOpt, 0, appendedOpts, args.length, rateMeteringConfigPathOpt.length);
return appendedOpts;
}
}
}

View file

@ -108,7 +108,7 @@ abstract class AbstractLinuxProcess implements LinuxProcess {
File bitcoindExecutable = Paths.get(config.bitcoinPath, "bitcoind").toFile(); File bitcoindExecutable = Paths.get(config.bitcoinPath, "bitcoind").toFile();
if (!bitcoindExecutable.exists() || !bitcoindExecutable.canExecute()) if (!bitcoindExecutable.exists() || !bitcoindExecutable.canExecute())
throw new IllegalStateException(format("'%s' cannot be found or executed.%n" throw new IllegalStateException(format("'%s' cannot be found or executed.%n"
+ "A bitcoin-core v0.19, v0.20, or v0.21 installation is required," + + "A bitcoin-core v0.19 - v22 installation is required," +
" and the 'bitcoinPath' must be configured in 'apitest.properties'", " and the 'bitcoinPath' must be configured in 'apitest.properties'",
bitcoindExecutable.getAbsolutePath())); bitcoindExecutable.getAbsolutePath()));

View file

@ -24,7 +24,7 @@ import java.util.List;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable;
import static bisq.apitest.config.ApiTestConfig.BASH_PATH_VALUE; import static bisq.apitest.config.ApiTestConfig.BASH_PATH_VALUE;
import static java.lang.management.ManagementFactory.getRuntimeMXBean; import static java.lang.management.ManagementFactory.getRuntimeMXBean;
@ -33,7 +33,9 @@ import static java.lang.management.ManagementFactory.getRuntimeMXBean;
public class BashCommand { public class BashCommand {
private int exitStatus = -1; private int exitStatus = -1;
@Nullable
private String output; private String output;
@Nullable
private String error; private String error;
private final String command; private final String command;
@ -92,6 +94,7 @@ public class BashCommand {
} }
// TODO return Optional<String> // TODO return Optional<String>
@Nullable
public String getOutput() { public String getOutput() {
return this.output; return this.output;
} }
@ -101,7 +104,6 @@ public class BashCommand {
return this.error; return this.error;
} }
@NotNull
private List<String> tokenizeSystemCommand() { private List<String> tokenizeSystemCommand() {
return new ArrayList<>() {{ return new ArrayList<>() {{
add(BASH_PATH_VALUE); add(BASH_PATH_VALUE);

View file

@ -57,15 +57,13 @@ class SystemCommandExecutor {
private ThreadedStreamHandler errorStreamHandler; private ThreadedStreamHandler errorStreamHandler;
public SystemCommandExecutor(final List<String> cmdOptions) { public SystemCommandExecutor(final List<String> cmdOptions) {
if (log.isDebugEnabled())
log.debug("cmd options {}", cmdOptions.toString());
if (cmdOptions.isEmpty()) if (cmdOptions.isEmpty())
throw new IllegalStateException("No command params specified."); throw new IllegalStateException("No command params specified.");
if (cmdOptions.contains("sudo")) if (cmdOptions.contains("sudo"))
throw new IllegalStateException("'sudo' commands are prohibited."); throw new IllegalStateException("'sudo' commands are prohibited.");
log.trace("System cmd options {}", cmdOptions);
this.cmdOptions = cmdOptions; this.cmdOptions = cmdOptions;
} }

View file

@ -0,0 +1,7 @@
# Haveno core properties file loaded by Haveno instances started by the test harness.
# Normally, it would be left empty, but it is useful for ad-hoc testing with
# Haveno Config options not configurable in test harness-specific apitest.properties
# file. This is where you might define Haveno options such as:
# dumpBlockchainData=true
# dumpDelayedPayoutTxs=true
# dumpStatistics=true

View file

@ -17,7 +17,8 @@
package bisq.apitest; package bisq.apitest;
import java.io.File; import java.time.Duration;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -29,23 +30,19 @@ import javax.annotation.Nullable;
import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestInfo;
import static bisq.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig;
import static bisq.apitest.config.HavenoAppConfig.alicedaemon; import static bisq.apitest.config.HavenoAppConfig.alicedaemon;
import static bisq.apitest.config.HavenoAppConfig.arbdaemon; import static bisq.apitest.config.HavenoAppConfig.arbdaemon;
import static bisq.apitest.config.HavenoAppConfig.bobdaemon; import static bisq.apitest.config.HavenoAppConfig.bobdaemon;
import static bisq.proto.grpc.DisputeAgentsGrpc.getRegisterDisputeAgentMethod; import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly;
import static bisq.proto.grpc.GetVersionGrpc.getGetVersionMethod;
import static java.net.InetAddress.getLoopbackAddress; import static java.net.InetAddress.getLoopbackAddress;
import static java.util.Arrays.stream; 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.ApiTestConfig;
import bisq.apitest.method.BitcoinCliHelper; import bisq.apitest.method.BitcoinCliHelper;
import bisq.cli.GrpcClient; 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'. * Base class for all test types: 'method', 'scenario' and 'e2e'.
@ -90,7 +87,7 @@ public class ApiTestCase {
throws InterruptedException, ExecutionException, IOException { throws InterruptedException, ExecutionException, IOException {
String[] params = new String[]{ String[] params = new String[]{
"--supportingApps", stream(supportingApps).map(Enum::name).collect(Collectors.joining(",")), "--supportingApps", stream(supportingApps).map(Enum::name).collect(Collectors.joining(",")),
"--callRateMeteringConfigPath", defaultRateMeterInterceptorConfig().getAbsolutePath(), "--callRateMeteringConfigPath", getTestRateMeterInterceptorConfig().getAbsolutePath(),
"--enableBisqDebugging", "false" "--enableBisqDebugging", "false"
}; };
setUpScaffold(params); setUpScaffold(params);
@ -136,11 +133,7 @@ public class ApiTestCase {
} }
protected static void sleep(long ms) { protected static void sleep(long ms) {
try { sleepUninterruptibly(Duration.ofMillis(ms));
MILLISECONDS.sleep(ms);
} catch (InterruptedException ignored) {
// empty
}
} }
protected final String testName(TestInfo testInfo) { protected final String testName(TestInfo testInfo) {
@ -148,37 +141,4 @@ public class ApiTestCase {
? testInfo.getTestMethod().get().getName() ? testInfo.getTestMethod().get().getName()
: "unknown test name"; : "unknown test name";
} }
protected static File defaultRateMeterInterceptorConfig() {
GrpcServiceRateMeteringConfig.Builder builder = new GrpcServiceRateMeteringConfig.Builder();
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
getGetVersionMethod().getFullMethodName(),
1,
SECONDS);
// Only GrpcVersionService is @VisibleForTesting, so we need to
// hardcode other grpcServiceClassName parameter values used in
// builder.addCallRateMeter(...).
builder.addCallRateMeter("GrpcDisputeAgentsService",
getRegisterDisputeAgentMethod().getFullMethodName(),
10, // Same as default.
SECONDS);
// Define rate meters for non-existent method 'disabled', to override other grpc
// services' default rate meters -- defined in their rateMeteringInterceptor()
// methods.
String[] serviceClassNames = new String[]{
"GrpcGetTradeStatisticsService",
"GrpcHelpService",
"GrpcOffersService",
"GrpcPaymentAccountsService",
"GrpcPriceService",
"GrpcTradesService",
"GrpcWalletsService"
};
for (String service : serviceClassNames) {
builder.addCallRateMeter(service, "disabled", 1, MILLISECONDS);
}
File file = builder.build();
file.deleteOnExit();
return file;
}
} }

View file

@ -19,17 +19,35 @@ package bisq.apitest.method;
import bisq.core.api.model.PaymentAccountForm; import bisq.core.api.model.PaymentAccountForm;
import bisq.core.payment.F2FAccount; import bisq.core.payment.F2FAccount;
import bisq.core.payment.NationalBankAccount;
import bisq.core.proto.CoreProtoResolver; import bisq.core.proto.CoreProtoResolver;
import bisq.common.util.Utilities; import bisq.common.util.Utilities;
import bisq.proto.grpc.BalancesInfo;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.math.BigDecimal;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger;
import javax.annotation.Nullable;
import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig;
import static bisq.cli.table.builder.TableType.BTC_BALANCE_TBL;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.stream; import static java.util.Arrays.stream;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@ -37,7 +55,9 @@ import static org.junit.jupiter.api.Assertions.fail;
import bisq.apitest.ApiTestCase; import bisq.apitest.ApiTestCase;
import bisq.apitest.linux.BashCommand;
import bisq.cli.GrpcClient; import bisq.cli.GrpcClient;
import bisq.cli.table.builder.TableBuilder;
public class MethodTest extends ApiTestCase { public class MethodTest extends ApiTestCase {
@ -46,15 +66,6 @@ public class MethodTest extends ApiTestCase {
private static final Function<Enum<?>[], String> toNameList = (enums) -> private static final Function<Enum<?>[], String> toNameList = (enums) ->
stream(enums).map(Enum::name).collect(Collectors.joining(",")); stream(enums).map(Enum::name).collect(Collectors.joining(","));
public static void startSupportingApps(File callRateMeteringConfigFile,
boolean generateBtcBlock,
Enum<?>... supportingApps) {
startSupportingApps(callRateMeteringConfigFile,
generateBtcBlock,
false,
supportingApps);
}
public static void startSupportingApps(File callRateMeteringConfigFile, public static void startSupportingApps(File callRateMeteringConfigFile,
boolean generateBtcBlock, boolean generateBtcBlock,
boolean startSupportingAppsInDebugMode, boolean startSupportingAppsInDebugMode,
@ -71,19 +82,12 @@ public class MethodTest extends ApiTestCase {
} }
} }
public static void startSupportingApps(boolean generateBtcBlock,
Enum<?>... supportingApps) {
startSupportingApps(generateBtcBlock,
false,
supportingApps);
}
public static void startSupportingApps(boolean generateBtcBlock, public static void startSupportingApps(boolean generateBtcBlock,
boolean startSupportingAppsInDebugMode, boolean startSupportingAppsInDebugMode,
Enum<?>... supportingApps) { Enum<?>... supportingApps) {
try { try {
// Disable call rate metering where there is no callRateMeteringConfigFile. // Disable call rate metering where there is no callRateMeteringConfigFile.
File callRateMeteringConfigFile = defaultRateMeterInterceptorConfig(); File callRateMeteringConfigFile = getTestRateMeterInterceptorConfig();
setUpScaffold(new String[]{ setUpScaffold(new String[]{
"--supportingApps", toNameList.apply(supportingApps), "--supportingApps", toNameList.apply(supportingApps),
"--callRateMeteringConfigPath", callRateMeteringConfigFile.getAbsolutePath(), "--callRateMeteringConfigPath", callRateMeteringConfigFile.getAbsolutePath(),
@ -133,17 +137,94 @@ public class MethodTest extends ApiTestCase {
return f2FAccount; return f2FAccount;
} }
protected bisq.core.payment.PaymentAccount createDummyBRLAccount(GrpcClient grpcClient,
String holderName,
String nationalAccountId,
String holderTaxId) {
String nationalBankAccountJsonString = "{\n" +
" \"_COMMENTS_\": [ \"Dummy Account\" ],\n" +
" \"paymentMethodId\": \"NATIONAL_BANK\",\n" +
" \"accountName\": \"Banco do Brasil\",\n" +
" \"country\": \"BR\",\n" +
" \"bankName\": \"Banco do Brasil\",\n" +
" \"branchId\": \"456789-10\",\n" +
" \"holderName\": \"" + holderName + "\",\n" +
" \"accountNr\": \"456789-87\",\n" +
" \"nationalAccountId\": \"" + nationalAccountId + "\",\n" +
" \"holderTaxId\": \"" + holderTaxId + "\"\n" +
"}\n";
NationalBankAccount nationalBankAccount =
(NationalBankAccount) createPaymentAccount(grpcClient, nationalBankAccountJsonString);
return nationalBankAccount;
}
protected final bisq.core.payment.PaymentAccount createPaymentAccount(GrpcClient grpcClient, String jsonString) { 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 // 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 // case we need a bisq.core.payment.PaymentAccount so it can be cast to its
// sub type. // sub-type.
var paymentAccount = grpcClient.createPaymentAccount(jsonString); var paymentAccount = grpcClient.createPaymentAccount(jsonString);
return bisq.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER); return bisq.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER);
} }
// Static conveniences for test methods and test case fixture setups. public static final Supplier<Double> defaultBuyerSecurityDepositPct = () -> {
var defaultPct = BigDecimal.valueOf(getDefaultBuyerSecurityDepositAsPercent());
if (defaultPct.precision() != 2)
throw new IllegalStateException(format(
"Unexpected decimal precision, expected 2 but actual is %d%n."
+ "Check for changes to Restrictions.getDefaultBuyerSecurityDepositAsPercent()",
defaultPct.precision()));
return defaultPct.movePointRight(2).doubleValue();
};
public static String formatBalancesTbls(BalancesInfo allBalances) {
StringBuilder balances = new StringBuilder(BTC).append("\n");
balances.append(new TableBuilder(BTC_BALANCE_TBL, allBalances.getBtc()).build());
balances.append("\n");
return balances.toString();
}
protected static String encodeToHex(String s) { protected static String encodeToHex(String s) {
return Utilities.bytesAsHexString(s.getBytes(UTF_8)); return Utilities.bytesAsHexString(s.getBytes(UTF_8));
} }
protected static Status.Code getStatusRuntimeExceptionStatusCode(Exception grpcException) {
if (grpcException instanceof StatusRuntimeException)
return ((StatusRuntimeException) grpcException).getStatus().getCode();
else
throw new IllegalArgumentException(
format("Expected a io.grpc.StatusRuntimeException argument, but got a %s",
grpcException.getClass().getName()));
}
protected void verifyNoLoggedNodeExceptions() {
var loggedExceptions = getNodeExceptionMessages();
if (loggedExceptions != null) {
String err = format("Exception(s) found in daemon log(s):%n%s", loggedExceptions);
fail(err);
}
}
protected void printNodeExceptionMessages(Logger log) {
var loggedExceptions = getNodeExceptionMessages();
if (loggedExceptions != null)
log.error("Exception(s) found in daemon log(s):\n{}", loggedExceptions);
}
@Nullable
protected static String getNodeExceptionMessages() {
var nodeLogsSpec = config.rootAppDataDir.getAbsolutePath() + "/bisq-BTC_REGTEST_*_dao/bisq.log";
var grep = "grep Exception " + nodeLogsSpec;
var bashCommand = new BashCommand(grep);
try {
bashCommand.run();
} catch (IOException | InterruptedException ex) {
fail("Bash command execution error: " + ex);
}
if (bashCommand.getError() == null)
return bashCommand.getOutput();
else
throw new IllegalStateException("Bash command execution error: " + bashCommand.getError());
}
} }

View file

@ -61,7 +61,7 @@ public class RegisterDisputeAgentsTest extends MethodTest {
public void testRegisterArbitratorShouldThrowException() { public void testRegisterArbitratorShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () -> Throwable exception = assertThrows(StatusRuntimeException.class, () ->
arbClient.registerDisputeAgent(ARBITRATOR, DEV_PRIVILEGE_PRIV_KEY)); arbClient.registerDisputeAgent(ARBITRATOR, DEV_PRIVILEGE_PRIV_KEY));
assertEquals("INVALID_ARGUMENT: arbitrators must be registered in a Bisq UI", assertEquals("UNIMPLEMENTED: arbitrators must be registered in a Bisq UI",
exception.getMessage()); exception.getMessage());
} }

View file

@ -17,13 +17,15 @@
package bisq.apitest.method.offer; package bisq.apitest.method.offer;
import bisq.core.monetary.Altcoin; import bisq.proto.grpc.OfferInfo;
import protobuf.PaymentAccount; import protobuf.PaymentAccount;
import org.bitcoinj.utils.Fiat;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.MathContext;
import java.util.List;
import java.util.function.Function;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -32,49 +34,96 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.ApiTestConfig.XMR;
import static bisq.apitest.config.HavenoAppConfig.alicedaemon; import static bisq.apitest.config.HavenoAppConfig.alicedaemon;
import static bisq.apitest.config.HavenoAppConfig.arbdaemon; import static bisq.apitest.config.HavenoAppConfig.arbdaemon;
import static bisq.apitest.config.HavenoAppConfig.bobdaemon; import static bisq.apitest.config.HavenoAppConfig.bobdaemon;
import static bisq.apitest.config.HavenoAppConfig.seednode; import static bisq.apitest.config.HavenoAppConfig.seednode;
import static bisq.common.util.MathUtils.roundDouble; import static bisq.cli.table.builder.TableType.OFFER_TBL;
import static bisq.common.util.MathUtils.scaleDownByPowerOf10; import static java.lang.String.format;
import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; import static java.lang.System.out;
import static java.math.RoundingMode.HALF_UP;
import bisq.apitest.method.MethodTest; import bisq.apitest.method.MethodTest;
import bisq.cli.CliMain;
import bisq.cli.table.builder.TableBuilder;
@Slf4j @Slf4j
public abstract class AbstractOfferTest extends MethodTest { public abstract class AbstractOfferTest extends MethodTest {
protected static final int ACTIVATE_OFFER = 1;
protected static final int DEACTIVATE_OFFER = 0;
protected static final String NO_TRIGGER_PRICE = "0";
@Setter @Setter
protected static boolean isLongRunningTest; protected static boolean isLongRunningTest;
protected static PaymentAccount alicesBtcAcct;
protected static PaymentAccount bobsBtcAcct;
protected static PaymentAccount alicesXmrAcct;
protected static PaymentAccount bobsXmrAcct;
@BeforeAll @BeforeAll
public static void setUp() { public static void setUp() {
setUp(false);
}
public static void setUp(boolean startSupportingAppsInDebugMode) {
startSupportingApps(true, startSupportingApps(true,
false, startSupportingAppsInDebugMode,
bitcoind, bitcoind,
seednode, seednode,
arbdaemon, arbdaemon,
alicedaemon, alicedaemon,
bobdaemon); bobdaemon);
initPaymentAccounts();
} }
protected double getScaledOfferPrice(double offerPrice, String currencyCode) { protected static final Function<OfferInfo, String> toOfferTable = (offer) ->
int precision = isCryptoCurrency(currencyCode) ? Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT; new TableBuilder(OFFER_TBL, offer).build().toString();
return scaleDownByPowerOf10(offerPrice, precision);
protected static final Function<List<OfferInfo>, String> toOffersTable = (offers) ->
new TableBuilder(OFFER_TBL, offers).build().toString();
protected static String calcPriceAsString(double base, double delta, int precision) {
var mathContext = new MathContext(precision);
var priceAsBigDecimal = new BigDecimal(Double.toString(base), mathContext)
.add(new BigDecimal(Double.toString(delta), mathContext))
.round(mathContext);
return format("%." + precision + "f", priceAsBigDecimal.doubleValue());
} }
protected final double getPercentageDifference(double price1, double price2) { @SuppressWarnings("ConstantConditions")
return BigDecimal.valueOf(roundDouble((1 - (price1 / price2)), 5)) public static void initPaymentAccounts() {
.setScale(4, HALF_UP) alicesBtcAcct = aliceClient.getPaymentAccount("BTC");
.doubleValue(); bobsBtcAcct = bobClient.getPaymentAccount("BTC");
}
@SuppressWarnings("ConstantConditions")
public static void createXmrPaymentAccounts() {
alicesXmrAcct = aliceClient.createCryptoCurrencyPaymentAccount("Alice's XMR Account",
XMR,
"44G4jWmSvTEfifSUZzTDnJVLPvYATmq9XhhtDqUof1BGCLceG82EQsVYG9Q9GN4bJcjbAJEc1JD1m5G7iK4UPZqACubV4Mq",
false);
log.trace("Alices XMR Account: {}", alicesXmrAcct);
bobsXmrAcct = bobClient.createCryptoCurrencyPaymentAccount("Bob's XMR Account",
XMR,
"4BDRhdSBKZqAXs3PuNTbMtaXBNqFj5idC2yMVnQj8Rm61AyKY8AxLTt9vGRJ8pwcG4EtpyD8YpGqdZWCZ2VZj6yVBN2RVKs",
false);
log.trace("Bob's XMR Account: {}", bobsXmrAcct);
} }
@AfterAll @AfterAll
public static void tearDown() { public static void tearDown() {
tearDownScaffold(); tearDownScaffold();
} }
protected static void runCliGetOffer(String offerId) {
out.println("Alice's CLI 'getmyoffer' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9998", "getmyoffer", "--offer-id=" + offerId});
out.println("Bob's CLI 'getoffer' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9999", "getoffer", "--offer-id=" + offerId});
}
} }

View file

@ -32,9 +32,8 @@ import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferDirection.BUY;
@Disabled @Disabled
@Slf4j @Slf4j
@ -51,8 +50,9 @@ public class CancelOfferTest extends AbstractOfferTest {
10000000L, 10000000L,
10000000L, 10000000L,
0.00, 0.00,
getDefaultBuyerSecurityDepositAsPercent(), defaultBuyerSecurityDepositPct.get(),
paymentAccountId); paymentAccountId,
NO_TRIGGER_PRICE);
}; };
@Test @Test

View file

@ -28,14 +28,14 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.XMR; import static bisq.apitest.config.ApiTestConfig.XMR;
import static bisq.cli.TableFormat.formatOfferTable; import static bisq.apitest.config.ApiTestConfig.EUR;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; import static bisq.apitest.config.ApiTestConfig.USD;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static protobuf.OfferPayload.Direction.BUY; import static org.junit.jupiter.api.Assertions.assertTrue;
import static protobuf.OfferPayload.Direction.SELL; import static protobuf.OfferDirection.BUY;
import static protobuf.OfferDirection.SELL;
@Disabled @Disabled
@Slf4j @Slf4j
@ -44,35 +44,44 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
@Test @Test
@Order(1) @Order(1)
public void testCreateAUDXMRBuyOfferUsingFixedPrice16000() { public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() {
PaymentAccount audAccount = createDummyF2FAccount(aliceClient, "AU"); PaymentAccount audAccount = createDummyF2FAccount(aliceClient, "AU");
var newOffer = aliceClient.createFixedPricedOffer(BUY.name(), var newOffer = aliceClient.createFixedPricedOffer(BUY.name(),
"aud", "aud",
10_000_000L, 10_000_000L,
10_000_000L, 10_000_000L,
"36000", "36000",
getDefaultBuyerSecurityDepositAsPercent(), defaultBuyerSecurityDepositPct.get(),
audAccount.getId()); audAccount.getId());
log.info("OFFER #1:\n{}", formatOfferTable(singletonList(newOffer), "AUD")); log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice()); assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals(360_000_000, newOffer.getPrice()); assertEquals("36000.0000", newOffer.getPrice());
assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals("3600", newOffer.getVolume());
assertEquals("3600", newOffer.getMinVolume());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(audAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(audAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals("AUD", newOffer.getCounterCurrencyCode()); assertEquals("AUD", newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice()); assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals(360_000_000, newOffer.getPrice()); assertEquals("36000.0000", newOffer.getPrice());
assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals("3600", newOffer.getVolume());
assertEquals("3600", newOffer.getMinVolume());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(audAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(audAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(XMR, newOffer.getBaseCurrencyCode());
@ -81,75 +90,93 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
@Test @Test
@Order(2) @Order(2)
public void testCreateUSDXMRBuyOfferUsingFixedPrice100001234() { public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() {
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US"); PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
var newOffer = aliceClient.createFixedPricedOffer(BUY.name(), var newOffer = aliceClient.createFixedPricedOffer(BUY.name(),
"usd", "usd",
10_000_000L, 10_000_000L,
10_000_000L, 10_000_000L,
"30000.1234", "30000.1234",
getDefaultBuyerSecurityDepositAsPercent(), defaultBuyerSecurityDepositPct.get(),
usdAccount.getId()); usdAccount.getId());
log.info("OFFER #2:\n{}", formatOfferTable(singletonList(newOffer), "USD")); log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice()); assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals(300_001_234, newOffer.getPrice()); assertEquals("30000.1234", newOffer.getPrice());
assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals("3000", newOffer.getVolume());
assertEquals("3000", newOffer.getMinVolume());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals("USD", newOffer.getCounterCurrencyCode()); assertEquals(USD, newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice()); assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals(300_001_234, newOffer.getPrice()); assertEquals("30000.1234", newOffer.getPrice());
assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals("3000", newOffer.getVolume());
assertEquals("3000", newOffer.getMinVolume());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals("USD", newOffer.getCounterCurrencyCode()); assertEquals(USD, newOffer.getCounterCurrencyCode());
} }
@Test @Test
@Order(3) @Order(3)
public void testCreateEURXMRSellOfferUsingFixedPrice95001234() { public void testCreateEURBTCSellOfferUsingFixedPrice95001234() {
PaymentAccount eurAccount = createDummyF2FAccount(aliceClient, "FR"); PaymentAccount eurAccount = createDummyF2FAccount(aliceClient, "FR");
var newOffer = aliceClient.createFixedPricedOffer(SELL.name(), var newOffer = aliceClient.createFixedPricedOffer(SELL.name(),
"eur", "eur",
10_000_000L, 10_000_000L,
5_000_000L, 5_000_000L,
"29500.1234", "29500.1234",
getDefaultBuyerSecurityDepositAsPercent(), defaultBuyerSecurityDepositPct.get(),
eurAccount.getId()); eurAccount.getId());
log.info("OFFER #3:\n{}", formatOfferTable(singletonList(newOffer), "EUR")); log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice()); assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals(295_001_234, newOffer.getPrice()); assertEquals("29500.1234", newOffer.getPrice());
assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount()); assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals("2950", newOffer.getVolume());
assertEquals("1475", newOffer.getMinVolume());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(eurAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(eurAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals("EUR", newOffer.getCounterCurrencyCode()); assertEquals(EUR, newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice()); assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals(295_001_234, newOffer.getPrice()); assertEquals("29500.1234", newOffer.getPrice());
assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount()); assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals("2950", newOffer.getVolume());
assertEquals("1475", newOffer.getMinVolume());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(eurAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(eurAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals("EUR", newOffer.getCounterCurrencyCode()); assertEquals(EUR, newOffer.getCounterCurrencyCode());
} }
} }

View file

@ -23,6 +23,8 @@ import bisq.proto.grpc.OfferInfo;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.math.BigDecimal;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
@ -31,20 +33,22 @@ import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.XMR; import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.cli.TableFormat.formatOfferTable; import static bisq.apitest.config.ApiTestConfig.USD;
import static bisq.common.util.MathUtils.roundDouble;
import static bisq.common.util.MathUtils.scaleDownByPowerOf10; import static bisq.common.util.MathUtils.scaleDownByPowerOf10;
import static bisq.common.util.MathUtils.scaleUpByPowerOf10; import static bisq.common.util.MathUtils.scaleUpByPowerOf10;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static java.lang.Math.abs; import static java.lang.Math.abs;
import static java.lang.String.format; import static java.lang.String.format;
import static java.util.Collections.singletonList; import static java.math.RoundingMode.HALF_UP;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferDirection.BUY;
import static protobuf.OfferPayload.Direction.SELL; import static protobuf.OfferDirection.SELL;
@SuppressWarnings("ConstantConditions")
@Disabled @Disabled
@Slf4j @Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@ -54,77 +58,95 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
private static final double MKT_PRICE_MARGIN_ERROR_TOLERANCE = 0.0050; // 0.50% private static final double MKT_PRICE_MARGIN_ERROR_TOLERANCE = 0.0050; // 0.50%
private static final double MKT_PRICE_MARGIN_WARNING_TOLERANCE = 0.0001; // 0.01% private static final double MKT_PRICE_MARGIN_WARNING_TOLERANCE = 0.0001; // 0.01%
private static final String MAKER_FEE_CURRENCY_CODE = BTC;
@Test @Test
@Order(1) @Order(1)
public void testCreateUSDXMRBuyOffer5PctPriceMargin() { public void testCreateUSDBTCBuyOffer5PctPriceMargin() {
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US"); PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
double priceMarginPctInput = 5.00; double priceMarginPctInput = 5.00d;
var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
"usd", "usd",
10_000_000L, 10_000_000L,
10_000_000L, 10_000_000L,
priceMarginPctInput, priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(), defaultBuyerSecurityDepositPct.get(),
usdAccount.getId()); usdAccount.getId(),
log.info("OFFER #1:\n{}", formatOfferTable(singletonList(newOffer), "usd")); NO_TRIGGER_PRICE);
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice()); assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("USD", newOffer.getCounterCurrencyCode()); assertEquals(USD, newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice()); assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("USD", newOffer.getCounterCurrencyCode()); assertEquals(USD, newOffer.getCounterCurrencyCode());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
} }
@Test @Test
@Order(2) @Order(2)
public void testCreateNZDXMRBuyOfferMinus2PctPriceMargin() { public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() {
PaymentAccount nzdAccount = createDummyF2FAccount(aliceClient, "NZ"); PaymentAccount nzdAccount = createDummyF2FAccount(aliceClient, "NZ");
double priceMarginPctInput = -2.00; double priceMarginPctInput = -2.00d; // -2%
var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
"nzd", "nzd",
10_000_000L, 10_000_000L,
10_000_000L, 10_000_000L,
priceMarginPctInput, priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(), defaultBuyerSecurityDepositPct.get(),
nzdAccount.getId()); nzdAccount.getId(),
log.info("OFFER #2:\n{}", formatOfferTable(singletonList(newOffer), "nzd")); NO_TRIGGER_PRICE);
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice()); assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(nzdAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(nzdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("NZD", newOffer.getCounterCurrencyCode()); assertEquals("NZD", newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice()); assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(nzdAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(nzdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("NZD", newOffer.getCounterCurrencyCode()); assertEquals("NZD", newOffer.getCounterCurrencyCode());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
@ -132,7 +154,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
@Test @Test
@Order(3) @Order(3)
public void testCreateGBPXMRSellOfferMinus1Point5PctPriceMargin() { public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() {
PaymentAccount gbpAccount = createDummyF2FAccount(aliceClient, "GB"); PaymentAccount gbpAccount = createDummyF2FAccount(aliceClient, "GB");
double priceMarginPctInput = -1.5; double priceMarginPctInput = -1.5;
var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(), var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(),
@ -140,29 +162,37 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L, 10_000_000L,
5_000_000L, 5_000_000L,
priceMarginPctInput, priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(), defaultBuyerSecurityDepositPct.get(),
gbpAccount.getId()); gbpAccount.getId(),
log.info("OFFER #3:\n{}", formatOfferTable(singletonList(newOffer), "gbp")); NO_TRIGGER_PRICE);
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice()); assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount()); assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(gbpAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(gbpAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("GBP", newOffer.getCounterCurrencyCode()); assertEquals("GBP", newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice()); assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount()); assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(gbpAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(gbpAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("GBP", newOffer.getCounterCurrencyCode()); assertEquals("GBP", newOffer.getCounterCurrencyCode());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
@ -170,7 +200,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
@Test @Test
@Order(4) @Order(4)
public void testCreateBRLXMRSellOffer6Point55PctPriceMargin() { public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() {
PaymentAccount brlAccount = createDummyF2FAccount(aliceClient, "BR"); PaymentAccount brlAccount = createDummyF2FAccount(aliceClient, "BR");
double priceMarginPctInput = 6.55; double priceMarginPctInput = 6.55;
var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(), var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(),
@ -178,53 +208,92 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L, 10_000_000L,
5_000_000L, 5_000_000L,
priceMarginPctInput, priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(), defaultBuyerSecurityDepositPct.get(),
brlAccount.getId()); brlAccount.getId(),
log.info("OFFER #4:\n{}", formatOfferTable(singletonList(newOffer), "brl")); NO_TRIGGER_PRICE);
log.debug("Offer #4:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice()); assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount()); assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(brlAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(brlAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("BRL", newOffer.getCounterCurrencyCode()); assertEquals("BRL", newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice()); assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount()); assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(brlAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(brlAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("BRL", newOffer.getCounterCurrencyCode()); assertEquals("BRL", newOffer.getCounterCurrencyCode());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
} }
@Test
@Order(5)
public void testCreateUSDBTCBuyOfferWithTriggerPrice() {
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
double mktPriceAsDouble = aliceClient.getBtcPrice("usd");
String triggerPrice = calcPriceAsString(mktPriceAsDouble, Double.parseDouble("1000.9999"), 4);
var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
"usd",
10_000_000L,
5_000_000L,
0.0,
defaultBuyerSecurityDepositPct.get(),
usdAccount.getId(),
triggerPrice);
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
genBtcBlocksThenWait(1, 4000); // give time to add to offer book
newOffer = aliceClient.getOffer(newOffer.getId());
log.debug("Offer #5:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(triggerPrice, newOffer.getTriggerPrice());
}
private void assertCalculatedPriceIsCorrect(OfferInfo offer, double priceMarginPctInput) { private void assertCalculatedPriceIsCorrect(OfferInfo offer, double priceMarginPctInput) {
assertTrue(() -> { assertTrue(() -> {
String counterCurrencyCode = offer.getCounterCurrencyCode(); String counterCurrencyCode = offer.getCounterCurrencyCode();
double mktPrice = aliceClient.getBtcPrice(counterCurrencyCode); double mktPrice = aliceClient.getBtcPrice(counterCurrencyCode);
double scaledOfferPrice = getScaledOfferPrice(offer.getPrice(), counterCurrencyCode); double priceAsDouble = Double.parseDouble(offer.getPrice());
double expectedDiffPct = scaleDownByPowerOf10(priceMarginPctInput, 2); double expectedDiffPct = scaleDownByPowerOf10(priceMarginPctInput, 2);
double actualDiffPct = offer.getDirection().equals(BUY.name()) double actualDiffPct = offer.getDirection().equals(BUY.name())
? getPercentageDifference(scaledOfferPrice, mktPrice) ? getPercentageDifference(priceAsDouble, mktPrice)
: getPercentageDifference(mktPrice, scaledOfferPrice); : getPercentageDifference(mktPrice, priceAsDouble);
double pctDiffDelta = abs(expectedDiffPct) - abs(actualDiffPct); double pctDiffDelta = abs(expectedDiffPct) - abs(actualDiffPct);
return isCalculatedPriceWithinErrorTolerance(pctDiffDelta, return isCalculatedPriceWithinErrorTolerance(pctDiffDelta,
expectedDiffPct, expectedDiffPct,
actualDiffPct, actualDiffPct,
mktPrice, mktPrice,
scaledOfferPrice, priceAsDouble,
offer); offer);
}); });
} }
private double getPercentageDifference(double price1, double price2) {
return BigDecimal.valueOf(roundDouble((1 - (price1 / price2)), 5))
.setScale(4, HALF_UP)
.doubleValue();
}
private boolean isCalculatedPriceWithinErrorTolerance(double delta, private boolean isCalculatedPriceWithinErrorTolerance(double delta,
double expectedDiffPct, double expectedDiffPct,
double actualDiffPct, double actualDiffPct,
@ -245,7 +314,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
actualDiffPct, actualDiffPct,
mktPrice, mktPrice,
scaledOfferPrice); scaledOfferPrice);
log.warn(offer.toString()); log.trace(offer.toString());
} }
return true; return true;

View file

@ -0,0 +1,265 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.method.offer;
import bisq.proto.grpc.OfferInfo;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.apitest.config.ApiTestConfig.XMR;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static protobuf.OfferDirection.BUY;
import static protobuf.OfferDirection.SELL;
@SuppressWarnings("ConstantConditions")
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CreateXMROffersTest extends AbstractOfferTest {
private static final String MAKER_FEE_CURRENCY_CODE = BTC;
@BeforeAll
public static void setUp() {
AbstractOfferTest.setUp();
createXmrPaymentAccounts();
}
@Test
@Order(1)
public void testCreateFixedPriceBuy1BTCFor200KXMROffer() {
// Remember alt coin trades are BTC trades. When placing an offer, you are
// offering to buy or sell BTC, not ETH, XMR, etc. In this test case,
// Alice places an offer to BUY BTC.
var newOffer = aliceClient.createFixedPricedOffer(BUY.name(),
XMR,
100_000_000L,
75_000_000L,
"0.005", // FIXED PRICE IN BTC FOR 1 XMR
defaultBuyerSecurityDepositPct.get(),
alicesXmrAcct.getId());
log.debug("Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals("0.00500000", newOffer.getPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(75_000_000L, newOffer.getMinAmount());
assertEquals(15_000_000, newOffer.getBuyerSecurityDeposit());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
genBtcBlockAndWaitForOfferPreparation();
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals("0.00500000", newOffer.getPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(75_000_000L, newOffer.getMinAmount());
assertEquals(15_000_000, newOffer.getBuyerSecurityDeposit());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
}
@Test
@Order(2)
public void testCreateFixedPriceSell1BTCFor200KXMROffer() {
// Alice places an offer to SELL BTC for XMR.
var newOffer = aliceClient.createFixedPricedOffer(SELL.name(),
XMR,
100_000_000L,
50_000_000L,
"0.005", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
defaultBuyerSecurityDepositPct.get(),
alicesXmrAcct.getId());
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals("0.00500000", newOffer.getPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(50_000_000L, newOffer.getMinAmount());
assertEquals(15_000_000, newOffer.getBuyerSecurityDeposit());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
genBtcBlockAndWaitForOfferPreparation();
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals("0.00500000", newOffer.getPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(50_000_000L, newOffer.getMinAmount());
assertEquals(15_000_000, newOffer.getBuyerSecurityDeposit());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
}
@Test
@Order(3)
public void testCreatePriceMarginBasedBuy1BTCOfferWithTriggerPrice() {
double priceMarginPctInput = 1.00;
double mktPriceAsDouble = aliceClient.getBtcPrice(XMR);
String triggerPrice = calcPriceAsString(mktPriceAsDouble, Double.parseDouble("-0.001"), 8);
var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
XMR,
100_000_000L,
75_000_000L,
priceMarginPctInput,
defaultBuyerSecurityDepositPct.get(),
alicesXmrAcct.getId(),
triggerPrice);
log.debug("Pending Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
// There is no trigger price while offer is pending.
assertEquals(NO_TRIGGER_PRICE, newOffer.getTriggerPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(75_000_000L, newOffer.getMinAmount());
assertEquals(15_000_000, newOffer.getBuyerSecurityDeposit());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
genBtcBlockAndWaitForOfferPreparation();
newOffer = aliceClient.getOffer(newOfferId);
log.debug("Available Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
// The trigger price should exist on the prepared offer.
assertEquals(triggerPrice, newOffer.getTriggerPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(75_000_000L, newOffer.getMinAmount());
assertEquals(15_000_000, newOffer.getBuyerSecurityDeposit());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
}
@Test
@Order(4)
public void testCreatePriceMarginBasedSell1BTCOffer() {
// Alice places an offer to SELL BTC for XMR.
double priceMarginPctInput = 0.50;
var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(),
XMR,
100_000_000L,
50_000_000L,
priceMarginPctInput,
defaultBuyerSecurityDepositPct.get(),
alicesXmrAcct.getId(),
NO_TRIGGER_PRICE);
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(50_000_000L, newOffer.getMinAmount());
assertEquals(15_000_000, newOffer.getBuyerSecurityDeposit());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
genBtcBlockAndWaitForOfferPreparation();
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(50_000_000L, newOffer.getMinAmount());
assertEquals(15_000_000, newOffer.getBuyerSecurityDeposit());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
}
@Test
@Order(5)
public void testGetAllMyXMROffers() {
List<OfferInfo> offers = aliceClient.getMyOffersSortedByDate(XMR);
log.debug("All of Alice's XMR offers:\n{}", toOffersTable.apply(offers));
assertEquals(4, offers.size());
log.debug("Alice's balances\n{}", formatBalancesTbls(aliceClient.getBalances()));
}
@Test
@Order(6)
public void testGetAvailableXMROffers() {
List<OfferInfo> offers = bobClient.getOffersSortedByDate(XMR);
log.debug("All of Bob's available XMR offers:\n{}", toOffersTable.apply(offers));
assertEquals(4, offers.size());
log.debug("Bob's balances\n{}", formatBalancesTbls(bobClient.getBalances()));
}
private void genBtcBlockAndWaitForOfferPreparation() {
genBtcBlocksThenWait(1, 5000);
}
}

View file

@ -30,11 +30,10 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BTC; import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static java.lang.String.format; import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferDirection.BUY;
@Disabled @Disabled
@Slf4j @Slf4j
@ -52,7 +51,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
100000000000L, // exceeds amount limit 100000000000L, // exceeds amount limit
100000000000L, 100000000000L,
"10000.0000", "10000.0000",
getDefaultBuyerSecurityDepositAsPercent(), defaultBuyerSecurityDepositPct.get(),
usdAccount.getId())); usdAccount.getId()));
assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage()); assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage());
} }
@ -68,7 +67,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
10000000L, 10000000L,
10000000L, 10000000L,
"40000.0000", "40000.0000",
getDefaultBuyerSecurityDepositAsPercent(), defaultBuyerSecurityDepositPct.get(),
chfAccount.getId())); chfAccount.getId()));
String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId()); String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId());
assertEquals(expectedError, exception.getMessage()); assertEquals(expectedError, exception.getMessage());
@ -85,7 +84,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
10000000L, 10000000L,
10000000L, 10000000L,
"63000.0000", "63000.0000",
getDefaultBuyerSecurityDepositAsPercent(), defaultBuyerSecurityDepositPct.get(),
audAccount.getId())); audAccount.getId()));
String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId()); String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId());
assertEquals(expectedError, exception.getMessage()); assertEquals(expectedError, exception.getMessage());

View file

@ -1,6 +1,7 @@
package bisq.apitest.method.payment; package bisq.apitest.method.payment;
import bisq.core.api.model.PaymentAccountForm; import bisq.core.api.model.PaymentAccountForm;
import bisq.core.locale.FiatCurrency;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.locale.TradeCurrency; import bisq.core.locale.TradeCurrency;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
@ -17,10 +18,13 @@ import java.io.IOException;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -57,14 +61,23 @@ public class AbstractPaymentAccountTest extends MethodTest {
static final String PROPERTY_NAME_BANK_ACCOUNT_NAME = "bankAccountName"; static final String PROPERTY_NAME_BANK_ACCOUNT_NAME = "bankAccountName";
static final String PROPERTY_NAME_BANK_ACCOUNT_NUMBER = "bankAccountNumber"; static final String PROPERTY_NAME_BANK_ACCOUNT_NUMBER = "bankAccountNumber";
static final String PROPERTY_NAME_BANK_ACCOUNT_TYPE = "bankAccountType"; static final String PROPERTY_NAME_BANK_ACCOUNT_TYPE = "bankAccountType";
static final String PROPERTY_NAME_BANK_ADDRESS = "bankAddress";
static final String PROPERTY_NAME_BANK_BRANCH = "bankBranch";
static final String PROPERTY_NAME_BANK_BRANCH_CODE = "bankBranchCode"; static final String PROPERTY_NAME_BANK_BRANCH_CODE = "bankBranchCode";
static final String PROPERTY_NAME_BANK_BRANCH_NAME = "bankBranchName"; static final String PROPERTY_NAME_BANK_BRANCH_NAME = "bankBranchName";
static final String PROPERTY_NAME_BANK_CODE = "bankCode"; static final String PROPERTY_NAME_BANK_CODE = "bankCode";
static final String PROPERTY_NAME_BANK_COUNTRY_CODE = "bankCountryCode";
@SuppressWarnings("unused") @SuppressWarnings("unused")
static final String PROPERTY_NAME_BANK_ID = "bankId"; static final String PROPERTY_NAME_BANK_ID = "bankId";
static final String PROPERTY_NAME_BANK_NAME = "bankName"; static final String PROPERTY_NAME_BANK_NAME = "bankName";
static final String PROPERTY_NAME_BANK_SWIFT_CODE = "bankSwiftCode";
static final String PROPERTY_NAME_BRANCH_ID = "branchId"; static final String PROPERTY_NAME_BRANCH_ID = "branchId";
static final String PROPERTY_NAME_BIC = "bic"; static final String PROPERTY_NAME_BIC = "bic";
static final String PROPERTY_NAME_BENEFICIARY_NAME = "beneficiaryName";
static final String PROPERTY_NAME_BENEFICIARY_ACCOUNT_NR = "beneficiaryAccountNr";
static final String PROPERTY_NAME_BENEFICIARY_ADDRESS = "beneficiaryAddress";
static final String PROPERTY_NAME_BENEFICIARY_CITY = "beneficiaryCity";
static final String PROPERTY_NAME_BENEFICIARY_PHONE = "beneficiaryPhone";
static final String PROPERTY_NAME_COUNTRY = "country"; static final String PROPERTY_NAME_COUNTRY = "country";
static final String PROPERTY_NAME_CITY = "city"; static final String PROPERTY_NAME_CITY = "city";
static final String PROPERTY_NAME_CONTACT = "contact"; static final String PROPERTY_NAME_CONTACT = "contact";
@ -75,6 +88,11 @@ public class AbstractPaymentAccountTest extends MethodTest {
static final String PROPERTY_NAME_HOLDER_NAME = "holderName"; static final String PROPERTY_NAME_HOLDER_NAME = "holderName";
static final String PROPERTY_NAME_HOLDER_TAX_ID = "holderTaxId"; static final String PROPERTY_NAME_HOLDER_TAX_ID = "holderTaxId";
static final String PROPERTY_NAME_IBAN = "iban"; static final String PROPERTY_NAME_IBAN = "iban";
static final String PROPERTY_NAME_INTERMEDIARY_ADDRESS = "intermediaryAddress";
static final String PROPERTY_NAME_INTERMEDIARY_BRANCH = "intermediaryBranch";
static final String PROPERTY_NAME_INTERMEDIARY_COUNTRY_CODE = "intermediaryCountryCode";
static final String PROPERTY_NAME_INTERMEDIARY_NAME = "intermediaryName";
static final String PROPERTY_NAME_INTERMEDIARY_SWIFT_CODE = "intermediarySwiftCode";
static final String PROPERTY_NAME_MOBILE_NR = "mobileNr"; static final String PROPERTY_NAME_MOBILE_NR = "mobileNr";
static final String PROPERTY_NAME_NATIONAL_ACCOUNT_ID = "nationalAccountId"; static final String PROPERTY_NAME_NATIONAL_ACCOUNT_ID = "nationalAccountId";
static final String PROPERTY_NAME_PAY_ID = "payid"; static final String PROPERTY_NAME_PAY_ID = "payid";
@ -83,7 +101,9 @@ public class AbstractPaymentAccountTest extends MethodTest {
static final String PROPERTY_NAME_QUESTION = "question"; static final String PROPERTY_NAME_QUESTION = "question";
static final String PROPERTY_NAME_REQUIREMENTS = "requirements"; static final String PROPERTY_NAME_REQUIREMENTS = "requirements";
static final String PROPERTY_NAME_SALT = "salt"; static final String PROPERTY_NAME_SALT = "salt";
static final String PROPERTY_NAME_SELECTED_TRADE_CURRENCY = "selectedTradeCurrency";
static final String PROPERTY_NAME_SORT_CODE = "sortCode"; static final String PROPERTY_NAME_SORT_CODE = "sortCode";
static final String PROPERTY_NAME_SPECIAL_INSTRUCTIONS = "specialInstructions";
static final String PROPERTY_NAME_STATE = "state"; static final String PROPERTY_NAME_STATE = "state";
static final String PROPERTY_NAME_TRADE_CURRENCIES = "tradeCurrencies"; static final String PROPERTY_NAME_TRADE_CURRENCIES = "tradeCurrencies";
static final String PROPERTY_NAME_USERNAME = "userName"; static final String PROPERTY_NAME_USERNAME = "userName";
@ -110,7 +130,7 @@ public class AbstractPaymentAccountTest extends MethodTest {
COMPLETED_FORM_MAP.clear(); COMPLETED_FORM_MAP.clear();
File emptyForm = getPaymentAccountForm(aliceClient, paymentMethodId); File emptyForm = getPaymentAccountForm(aliceClient, paymentMethodId);
// A short cut over the API: // A shortcut over the API:
// File emptyForm = PAYMENT_ACCOUNT_FORM.getPaymentAccountForm(paymentMethodId); // File emptyForm = PAYMENT_ACCOUNT_FORM.getPaymentAccountForm(paymentMethodId);
log.debug("{} Empty form saved to {}", log.debug("{} Empty form saved to {}",
testName(testInfo), testName(testInfo),
@ -125,7 +145,13 @@ public class AbstractPaymentAccountTest extends MethodTest {
PAYMENT_ACCOUNT_FORM.toJsonString(jsonForm), PAYMENT_ACCOUNT_FORM.toJsonString(jsonForm),
Object.class); Object.class);
assertNotNull(emptyForm); assertNotNull(emptyForm);
assertEquals(PROPERTY_VALUE_JSON_COMMENTS, emptyForm.get(PROPERTY_NAME_JSON_COMMENTS));
if (paymentMethodId.equals("SWIFT_ID")) {
assertEquals(getSwiftFormComments(), emptyForm.get(PROPERTY_NAME_JSON_COMMENTS));
} else {
assertEquals(PROPERTY_VALUE_JSON_COMMENTS, emptyForm.get(PROPERTY_NAME_JSON_COMMENTS));
}
assertEquals(paymentMethodId, emptyForm.get(PROPERTY_NAME_PAYMENT_METHOD_ID)); assertEquals(paymentMethodId, emptyForm.get(PROPERTY_NAME_PAYMENT_METHOD_ID));
assertEquals("your accountname", emptyForm.get(PROPERTY_NAME_ACCOUNT_NAME)); assertEquals("your accountname", emptyForm.get(PROPERTY_NAME_ACCOUNT_NAME));
for (String field : fields) { for (String field : fields) {
@ -149,6 +175,15 @@ public class AbstractPaymentAccountTest extends MethodTest {
assertEquals(expectedCurrencyCode, paymentAccount.getSingleTradeCurrency().getCode()); assertEquals(expectedCurrencyCode, paymentAccount.getSingleTradeCurrency().getCode());
} }
protected final void verifyAccountTradeCurrencies(Collection<FiatCurrency> expectedFiatCurrencies,
PaymentAccount paymentAccount) {
assertNotNull(paymentAccount.getTradeCurrencies());
List<TradeCurrency> expectedTradeCurrencies = new ArrayList<>() {{
addAll(expectedFiatCurrencies);
}};
assertArrayEquals(expectedTradeCurrencies.toArray(), paymentAccount.getTradeCurrencies().toArray());
}
protected final void verifyAccountTradeCurrencies(List<TradeCurrency> expectedTradeCurrencies, protected final void verifyAccountTradeCurrencies(List<TradeCurrency> expectedTradeCurrencies,
PaymentAccount paymentAccount) { PaymentAccount paymentAccount) {
assertNotNull(paymentAccount.getTradeCurrencies()); assertNotNull(paymentAccount.getTradeCurrencies());
@ -164,14 +199,36 @@ public class AbstractPaymentAccountTest extends MethodTest {
assertTrue(paymentAccount.isPresent()); assertTrue(paymentAccount.isPresent());
} }
protected final String getCompletedFormAsJsonString() { protected final String getCompletedFormAsJsonString(List<String> comments) {
File completedForm = fillPaymentAccountForm(); File completedForm = fillPaymentAccountForm(comments);
String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm); String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
log.debug("Completed form: {}", jsonString); log.debug("Completed form: {}", jsonString);
return jsonString; return jsonString;
} }
private File fillPaymentAccountForm() { protected final String getCompletedFormAsJsonString() {
File completedForm = fillPaymentAccountForm(PROPERTY_VALUE_JSON_COMMENTS);
String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
log.debug("Completed form: {}", jsonString);
return jsonString;
}
protected final String getCommaDelimitedFiatCurrencyCodes(Collection<FiatCurrency> fiatCurrencies) {
return fiatCurrencies.stream()
.sorted(Comparator.comparing(TradeCurrency::getCode))
.map(c -> c.getCurrency().getCurrencyCode())
.collect(Collectors.joining(","));
}
protected final List<String> getSwiftFormComments() {
List<String> comments = new ArrayList<>();
comments.addAll(PROPERTY_VALUE_JSON_COMMENTS);
List<String> wrappedSwiftComments = Res.getWrappedAsList("payment.swift.info.account", 110);
comments.addAll(wrappedSwiftComments);
return comments;
}
private File fillPaymentAccountForm(List<String> comments) {
File tmpJsonForm = null; File tmpJsonForm = null;
try { try {
tmpJsonForm = File.createTempFile("temp_acct_form_", tmpJsonForm = File.createTempFile("temp_acct_form_",
@ -182,7 +239,7 @@ public class AbstractPaymentAccountTest extends MethodTest {
writer.name(PROPERTY_NAME_JSON_COMMENTS); writer.name(PROPERTY_NAME_JSON_COMMENTS);
writer.beginArray(); writer.beginArray();
for (String s : PROPERTY_VALUE_JSON_COMMENTS) { for (String s : comments) {
writer.value(s); writer.value(s);
} }
writer.endArray(); writer.endArray();

View file

@ -17,12 +17,13 @@
package bisq.apitest.method.payment; package bisq.apitest.method.payment;
import bisq.core.locale.FiatCurrency;
import bisq.core.locale.TradeCurrency; import bisq.core.locale.TradeCurrency;
import bisq.core.payment.AdvancedCashAccount; import bisq.core.payment.AdvancedCashAccount;
import bisq.core.payment.AliPayAccount; import bisq.core.payment.AliPayAccount;
import bisq.core.payment.AustraliaPayid; import bisq.core.payment.AustraliaPayidAccount;
import bisq.core.payment.CapitualAccount;
import bisq.core.payment.CashDepositAccount; import bisq.core.payment.CashDepositAccount;
import bisq.core.payment.ChaseQuickPayAccount;
import bisq.core.payment.ClearXchangeAccount; import bisq.core.payment.ClearXchangeAccount;
import bisq.core.payment.F2FAccount; import bisq.core.payment.F2FAccount;
import bisq.core.payment.FasterPaymentsAccount; import bisq.core.payment.FasterPaymentsAccount;
@ -32,7 +33,9 @@ import bisq.core.payment.JapanBankAccount;
import bisq.core.payment.MoneyBeamAccount; import bisq.core.payment.MoneyBeamAccount;
import bisq.core.payment.MoneyGramAccount; import bisq.core.payment.MoneyGramAccount;
import bisq.core.payment.NationalBankAccount; import bisq.core.payment.NationalBankAccount;
import bisq.core.payment.PaxumAccount;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
import bisq.core.payment.PayseraAccount;
import bisq.core.payment.PerfectMoneyAccount; import bisq.core.payment.PerfectMoneyAccount;
import bisq.core.payment.PopmoneyAccount; import bisq.core.payment.PopmoneyAccount;
import bisq.core.payment.PromptPayAccount; import bisq.core.payment.PromptPayAccount;
@ -41,6 +44,7 @@ import bisq.core.payment.SameBankAccount;
import bisq.core.payment.SepaAccount; import bisq.core.payment.SepaAccount;
import bisq.core.payment.SepaInstantAccount; import bisq.core.payment.SepaInstantAccount;
import bisq.core.payment.SpecificBanksAccount; import bisq.core.payment.SpecificBanksAccount;
import bisq.core.payment.SwiftAccount;
import bisq.core.payment.SwishAccount; import bisq.core.payment.SwishAccount;
import bisq.core.payment.TransferwiseAccount; import bisq.core.payment.TransferwiseAccount;
import bisq.core.payment.USPostalMoneyOrderAccount; import bisq.core.payment.USPostalMoneyOrderAccount;
@ -51,14 +55,17 @@ import bisq.core.payment.payload.BankAccountPayload;
import bisq.core.payment.payload.CashDepositAccountPayload; import bisq.core.payment.payload.CashDepositAccountPayload;
import bisq.core.payment.payload.SameBankAccountPayload; import bisq.core.payment.payload.SameBankAccountPayload;
import bisq.core.payment.payload.SpecificBanksAccountPayload; import bisq.core.payment.payload.SpecificBanksAccountPayload;
import bisq.core.payment.payload.SwiftAccountPayload;
import io.grpc.StatusRuntimeException; import io.grpc.StatusRuntimeException;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -70,16 +77,23 @@ import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.ApiTestConfig.EUR;
import static bisq.apitest.config.ApiTestConfig.USD;
import static bisq.apitest.config.HavenoAppConfig.alicedaemon; import static bisq.apitest.config.HavenoAppConfig.alicedaemon;
import static bisq.cli.TableFormat.formatPaymentAcctTbl; import static bisq.cli.table.builder.TableType.PAYMENT_ACCOUNT_TBL;
import static bisq.core.locale.CurrencyUtil.*; import static bisq.core.locale.CurrencyUtil.getAllSortedFiatCurrencies;
import static bisq.core.locale.CurrencyUtil.getTradeCurrency;
import static bisq.core.payment.payload.PaymentMethod.*; import static bisq.core.payment.payload.PaymentMethod.*;
import static java.util.Collections.singletonList; import static java.util.Comparator.comparing;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import bisq.cli.table.builder.TableBuilder;
@SuppressWarnings({"OptionalGetWithoutIsPresent", "ConstantConditions"}) @SuppressWarnings({"OptionalGetWithoutIsPresent", "ConstantConditions"})
@Disabled @Disabled
@Slf4j @Slf4j
@ -104,11 +118,18 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ADVANCED_CASH_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ADVANCED_CASH_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Advanced Cash Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Advanced Cash Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "0000 1111 2222"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "0000 1111 2222");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, AdvancedCashAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "RUB");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Advanced Cash Acct Salt")); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Advanced Cash Acct Salt"));
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
AdvancedCashAccount paymentAccount = (AdvancedCashAccount) createPaymentAccount(aliceClient, jsonString); AdvancedCashAccount paymentAccount = (AdvancedCashAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(getAllAdvancedCashCurrencies(), paymentAccount); verifyAccountTradeCurrencies(AdvancedCashAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
@ -146,7 +167,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Credit Union Australia"); 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")); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Australia Pay ID Acct Salt"));
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
AustraliaPayid paymentAccount = (AustraliaPayid) createPaymentAccount(aliceClient, jsonString); AustraliaPayidAccount paymentAccount = (AustraliaPayidAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("AUD", paymentAccount); verifyAccountSingleTradeCurrency("AUD", paymentAccount);
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
@ -156,6 +177,33 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
print(paymentAccount); print(paymentAccount);
} }
@Test
public void testCreateCapitualAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, CAPITUAL_ID);
verifyEmptyForm(emptyForm,
CAPITUAL_ID,
PROPERTY_NAME_ACCOUNT_NR);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CAPITUAL_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Capitual Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "1111 2222 3333-4");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, CapitualAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "BRL");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Capitual Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
CapitualAccount paymentAccount = (CapitualAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(CapitualAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test @Test
public void testCreateCashDepositAccount(TestInfo testInfo) { public void testCreateCashDepositAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, CASH_DEPOSIT_ID); File emptyForm = getEmptyForm(testInfo, CASH_DEPOSIT_ID);
@ -189,7 +237,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
CashDepositAccount paymentAccount = (CashDepositAccount) createPaymentAccount(aliceClient, jsonString); CashDepositAccount paymentAccount = (CashDepositAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("EUR", paymentAccount); verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code); Objects.requireNonNull(paymentAccount.getCountry()).code);
@ -253,28 +301,6 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
print(paymentAccount); print(paymentAccount);
} }
@Test
public void testCreateChaseQuickPayAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, CHASE_QUICK_PAY_ID);
verifyEmptyForm(emptyForm,
CHASE_QUICK_PAY_ID,
PROPERTY_NAME_EMAIL,
PROPERTY_NAME_HOLDER_NAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CHASE_QUICK_PAY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Quick Pay Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "johndoe@quickpay.com");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
ChaseQuickPayAccount paymentAccount = (ChaseQuickPayAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
print(paymentAccount);
}
@Test @Test
public void testCreateClearXChangeAccount(TestInfo testInfo) { public void testCreateClearXChangeAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, CLEAR_X_CHANGE_ID); File emptyForm = getEmptyForm(testInfo, CLEAR_X_CHANGE_ID);
@ -290,7 +316,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
ClearXchangeAccount paymentAccount = (ClearXchangeAccount) createPaymentAccount(aliceClient, jsonString); ClearXchangeAccount paymentAccount = (ClearXchangeAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount); verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL_OR_MOBILE_NR), paymentAccount.getEmailOrMobileNr()); 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_HOLDER_NAME), paymentAccount.getHolderName());
@ -363,7 +389,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
HalCashAccount paymentAccount = (HalCashAccount) createPaymentAccount(aliceClient, jsonString); HalCashAccount paymentAccount = (HalCashAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("EUR", paymentAccount); verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr());
print(paymentAccount); print(paymentAccount);
@ -448,7 +474,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
MoneyBeamAccount paymentAccount = (MoneyBeamAccount) createPaymentAccount(aliceClient, jsonString); MoneyBeamAccount paymentAccount = (MoneyBeamAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("EUR", paymentAccount); verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
@ -466,6 +492,11 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
PROPERTY_NAME_STATE); PROPERTY_NAME_STATE);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_GRAM_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_GRAM_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Gram Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Gram Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, MoneyGramAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "INR");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "john@doe.info"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "john@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "US"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "US");
@ -474,7 +505,9 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
MoneyGramAccount paymentAccount = (MoneyGramAccount) createPaymentAccount(aliceClient, jsonString); MoneyGramAccount paymentAccount = (MoneyGramAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(getAllMoneyGramCurrencies(), paymentAccount); verifyAccountTradeCurrencies(MoneyGramAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
@ -497,13 +530,65 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
PerfectMoneyAccount paymentAccount = (PerfectMoneyAccount) createPaymentAccount(aliceClient, jsonString); PerfectMoneyAccount paymentAccount = (PerfectMoneyAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount); verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount); print(paymentAccount);
} }
@Test
public void testCreatePaxumAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, PAXUM_ID);
verifyEmptyForm(emptyForm,
PAXUM_ID,
PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PAXUM_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Paxum Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, PaxumAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "SEK");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.net");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
PaxumAccount paymentAccount = (PaxumAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(PaxumAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
print(paymentAccount);
}
@Test
public void testCreatePayseraAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, PAYSERA_ID);
verifyEmptyForm(emptyForm,
PAYSERA_ID,
PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PAYSERA_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Paysera Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, PayseraAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "ZAR");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.net");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
PayseraAccount paymentAccount = (PayseraAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(PayseraAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
print(paymentAccount);
}
@Test @Test
public void testCreatePopmoneyAccount(TestInfo testInfo) { public void testCreatePopmoneyAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, POPMONEY_ID); File emptyForm = getEmptyForm(testInfo, POPMONEY_ID);
@ -519,7 +604,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
PopmoneyAccount paymentAccount = (PopmoneyAccount) createPaymentAccount(aliceClient, jsonString); PopmoneyAccount paymentAccount = (PopmoneyAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount); verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
@ -554,12 +639,19 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
PROPERTY_NAME_USERNAME); PROPERTY_NAME_USERNAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, REVOLUT_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, REVOLUT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Revolut Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Revolut Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, RevolutAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "QAR");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_USERNAME, "revolut123"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_USERNAME, "revolut123");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
RevolutAccount paymentAccount = (RevolutAccount) createPaymentAccount(aliceClient, jsonString); RevolutAccount paymentAccount = (RevolutAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(getAllRevolutCurrencies(), paymentAccount); verifyAccountTradeCurrencies(RevolutAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_USERNAME), paymentAccount.getUserName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_USERNAME), paymentAccount.getUserName());
print(paymentAccount); print(paymentAccount);
@ -631,7 +723,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code); Objects.requireNonNull(paymentAccount.getCountry()).code);
verifyAccountSingleTradeCurrency("EUR", paymentAccount); verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban());
@ -662,7 +754,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code); Objects.requireNonNull(paymentAccount.getCountry()).code);
verifyAccountSingleTradeCurrency("EUR", paymentAccount); verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban());
@ -720,6 +812,64 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
print(paymentAccount); print(paymentAccount);
} }
@Test
public void testCreateSwiftAccount(TestInfo testInfo) {
// https://www.theswiftcodes.com
File emptyForm = getEmptyForm(testInfo, SWIFT_ID);
verifyEmptyForm(emptyForm,
SWIFT_ID,
PROPERTY_NAME_BANK_SWIFT_CODE);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SWIFT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "IT Swift Acct w/ DE Intermediary");
Collection<FiatCurrency> swiftCurrenciesSortedByCode = getAllSortedFiatCurrencies(comparing(TradeCurrency::getCode));
String allFiatCodes = getCommaDelimitedFiatCurrencyCodes(swiftCurrenciesSortedByCode);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, allFiatCodes);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, EUR);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_SWIFT_CODE, "PASCITMMFIR");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_COUNTRY_CODE, "IT");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "BANCA MONTE DEI PASCHI DI SIENA S.P.A.");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH, "SUCC. DI FIRENZE");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ADDRESS, "Via dei Pecori, 8, 50123 Firenze FI, Italy");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_NAME, "Vito de' Medici");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_ACCOUNT_NR, "0000 1111 2222 3333");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_ADDRESS, "Via dei Pecori, 1, 50123 Firenze FI, Italy");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_CITY, "Firenze");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_PHONE, "+39 055 222222");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SPECIAL_INSTRUCTIONS, "N/A");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_SWIFT_CODE, "DEUTDEFFXXX");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_COUNTRY_CODE, "DE");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_NAME, "Kosmo Krump");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_ADDRESS, "TAUNUSANLAGE 12, FRANKFURT AM MAIN, 60262");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_BRANCH, "Deutsche Bank Frankfurt F");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Swift Acct Salt"));
String jsonString = getCompletedFormAsJsonString(getSwiftFormComments());
SwiftAccount paymentAccount = (SwiftAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(swiftCurrenciesSortedByCode, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
SwiftAccountPayload payload = (SwiftAccountPayload) paymentAccount.getPaymentAccountPayload();
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_SWIFT_CODE), payload.getBankSwiftCode());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_COUNTRY_CODE), payload.getBankCountryCode());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH), payload.getBankBranch());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ADDRESS), payload.getBankAddress());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_NAME), payload.getBeneficiaryName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_ACCOUNT_NR), payload.getBeneficiaryAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_ADDRESS), payload.getBeneficiaryAddress());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_CITY), payload.getBeneficiaryCity());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_PHONE), payload.getBeneficiaryPhone());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SPECIAL_INSTRUCTIONS), payload.getSpecialInstructions());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_SWIFT_CODE), payload.getIntermediarySwiftCode());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_COUNTRY_CODE), payload.getIntermediaryCountryCode());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_NAME), payload.getIntermediaryName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_BRANCH), payload.getIntermediaryBranch());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_ADDRESS), payload.getIntermediaryAddress());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test @Test
public void testCreateSwishAccount(TestInfo testInfo) { public void testCreateSwishAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, SWISH_ID); File emptyForm = getEmptyForm(testInfo, SWISH_ID);
@ -751,17 +901,16 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
PROPERTY_NAME_EMAIL); PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID); 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_ACCOUNT_NAME, "Transferwise Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, "eur"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, "NZD");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "NZD");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(aliceClient, jsonString); TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
assertEquals(1, paymentAccount.getTradeCurrencies().size()); assertEquals(1, paymentAccount.getTradeCurrencies().size());
TradeCurrency expectedCurrency = getTradeCurrency("EUR").get(); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
assertEquals(expectedCurrency, paymentAccount.getSelectedTradeCurrency()); paymentAccount.getSelectedTradeCurrency().getCode());
List<TradeCurrency> expectedTradeCurrencies = singletonList(expectedCurrency);
verifyAccountTradeCurrencies(expectedTradeCurrencies, paymentAccount);
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
print(paymentAccount); print(paymentAccount);
@ -775,7 +924,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
PROPERTY_NAME_EMAIL); PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID); 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_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_TRADE_CURRENCIES, "ARS,CAD,HRK,CZK,EUR,HKD,IDR,JPY,CHF,NZD");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "CHF");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
@ -787,7 +937,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
add(getTradeCurrency("CAD").get()); add(getTradeCurrency("CAD").get());
add(getTradeCurrency("HRK").get()); add(getTradeCurrency("HRK").get());
add(getTradeCurrency("CZK").get()); add(getTradeCurrency("CZK").get());
add(getTradeCurrency("EUR").get()); add(getTradeCurrency(EUR).get());
add(getTradeCurrency("HKD").get()); add(getTradeCurrency("HKD").get());
add(getTradeCurrency("IDR").get()); add(getTradeCurrency("IDR").get());
add(getTradeCurrency("JPY").get()); add(getTradeCurrency("JPY").get());
@ -795,8 +945,34 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
add(getTradeCurrency("NZD").get()); add(getTradeCurrency("NZD").get());
}}; }};
verifyAccountTradeCurrencies(expectedTradeCurrencies, paymentAccount); verifyAccountTradeCurrencies(expectedTradeCurrencies, paymentAccount);
TradeCurrency expectedSelectedCurrency = expectedTradeCurrencies.get(0); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
assertEquals(expectedSelectedCurrency, paymentAccount.getSelectedTradeCurrency()); paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
print(paymentAccount);
}
@Test
public void testCreateTransferwiseAccountWithSupportedTradeCurrencies(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, TransferwiseAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "AUD");
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());
verifyAccountTradeCurrencies(TransferwiseAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
print(paymentAccount); print(paymentAccount);
@ -836,7 +1012,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
Throwable exception = assertThrows(StatusRuntimeException.class, () -> Throwable exception = assertThrows(StatusRuntimeException.class, () ->
createPaymentAccount(aliceClient, jsonString)); createPaymentAccount(aliceClient, jsonString));
assertEquals("INVALID_ARGUMENT: no trade currencies defined for transferwise payment account", assertEquals("INVALID_ARGUMENT: no trade currency defined for transferwise payment account",
exception.getMessage()); exception.getMessage());
} }
@ -849,11 +1025,18 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, UPHOLD_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, UPHOLD_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Uphold Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Uphold Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "UA 9876"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "UA 9876");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, UpholdAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "MXN");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Uphold Acct Salt")); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Uphold Acct Salt"));
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
UpholdAccount paymentAccount = (UpholdAccount) createPaymentAccount(aliceClient, jsonString); UpholdAccount paymentAccount = (UpholdAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(getAllUpholdCurrencies(), paymentAccount); verifyAccountTradeCurrencies(UpholdAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
@ -875,7 +1058,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
USPostalMoneyOrderAccount paymentAccount = (USPostalMoneyOrderAccount) createPaymentAccount(aliceClient, jsonString); USPostalMoneyOrderAccount paymentAccount = (USPostalMoneyOrderAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount); verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_POSTAL_ADDRESS), paymentAccount.getPostalAddress()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_POSTAL_ADDRESS), paymentAccount.getPostalAddress());
@ -923,7 +1106,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString(); String jsonString = getCompletedFormAsJsonString();
WesternUnionAccount paymentAccount = (WesternUnionAccount) createPaymentAccount(aliceClient, jsonString); WesternUnionAccount paymentAccount = (WesternUnionAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount); verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount); verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity());
@ -942,7 +1125,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
private void print(PaymentAccount paymentAccount) { private void print(PaymentAccount paymentAccount) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
log.debug("\n{}", formatPaymentAcctTbl(singletonList(paymentAccount.toProtoMessage()))); log.debug("\n{}", new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount.toProtoMessage()).build());
} }
} }
} }

View file

@ -2,28 +2,47 @@ package bisq.apitest.method.trade;
import bisq.proto.grpc.TradeInfo; import bisq.proto.grpc.TradeInfo;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.slf4j.Logger; import org.slf4j.Logger;
import lombok.Getter;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestInfo;
import static bisq.cli.TradeFormat.format; import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL;
import static org.junit.jupiter.api.Assertions.assertEquals; import static bisq.core.trade.Trade.Phase.DEPOSIT_UNLOCKED;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static bisq.core.trade.Trade.Phase.PAYMENT_SENT;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
import static bisq.core.trade.Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG;
import static bisq.core.trade.Trade.State.DEPOSIT_UNLOCKED_IN_BLOCK_CHAIN;
import static bisq.core.trade.Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG;
import static java.lang.String.format;
import static java.lang.System.out;
import static org.junit.jupiter.api.Assertions.*;
import bisq.apitest.method.offer.AbstractOfferTest; import bisq.apitest.method.offer.AbstractOfferTest;
import bisq.cli.CliMain;
import bisq.cli.GrpcClient;
import bisq.cli.table.builder.TableBuilder;
public class AbstractTradeTest extends AbstractOfferTest { public class AbstractTradeTest extends AbstractOfferTest {
public static final ExpectedProtocolStatus EXPECTED_PROTOCOL_STATUS = new ExpectedProtocolStatus(); public static final ExpectedProtocolStatus EXPECTED_PROTOCOL_STATUS = new ExpectedProtocolStatus();
// A Trade ID cache for use in @Test sequences. // A Trade ID cache for use in @Test sequences.
@Getter
protected static String tradeId; protected static String tradeId;
protected final Supplier<Integer> maxTradeStateAndPhaseChecks = () -> isLongRunningTest ? 10 : 2; protected final Supplier<Integer> maxTradeStateAndPhaseChecks = () -> isLongRunningTest ? 10 : 2;
protected final Function<TradeInfo, String> toTradeDetailTable = (trade) ->
new TableBuilder(TRADE_DETAIL_TBL, trade).build().toString();
protected final Function<GrpcClient, String> toUserName = (client) -> client.equals(aliceClient) ? "Alice" : "Bob";
@BeforeAll @BeforeAll
public static void initStaticFixtures() { public static void initStaticFixtures() {
@ -32,13 +51,129 @@ public class AbstractTradeTest extends AbstractOfferTest {
protected final TradeInfo takeAlicesOffer(String offerId, protected final TradeInfo takeAlicesOffer(String offerId,
String paymentAccountId) { String paymentAccountId) {
return bobClient.takeOffer(offerId, paymentAccountId); return takeAlicesOffer(offerId,
paymentAccountId,
true);
} }
@SuppressWarnings("unused") protected final TradeInfo takeAlicesOffer(String offerId,
protected final TradeInfo takeBobsOffer(String offerId, String paymentAccountId,
String paymentAccountId) { boolean generateBtcBlock) {
return aliceClient.takeOffer(offerId, paymentAccountId); @SuppressWarnings("ConstantConditions")
var trade = bobClient.takeOffer(offerId,
paymentAccountId);
assertNotNull(trade);
assertEquals(offerId, trade.getTradeId());
// Cache the trade id for the other tests.
tradeId = trade.getTradeId();
if (generateBtcBlock)
genBtcBlocksThenWait(1, 6_000);
return trade;
}
protected final void waitForDepositConfirmation(Logger log,
TestInfo testInfo,
GrpcClient grpcClient,
String tradeId) {
Predicate<TradeInfo> isTradeInDepositUnlockedStateAndPhase = (t) ->
t.getState().equals(DEPOSIT_UNLOCKED_IN_BLOCK_CHAIN.name())
&& t.getPhase().equals(DEPOSIT_UNLOCKED.name());
String userName = toUserName.apply(grpcClient);
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
TradeInfo trade = grpcClient.getTrade(tradeId);
if (!isTradeInDepositUnlockedStateAndPhase.test(trade)) {
log.warn("{} still waiting on trade {} tx {}: DEPOSIT_UNLOCKED_IN_BLOCK_CHAIN, attempt # {}",
userName,
trade.getShortId(),
trade.getMakerDepositTxId(),
trade.getTakerDepositTxId(),
i);
genBtcBlocksThenWait(1, 4_000);
} else {
EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_UNLOCKED_IN_BLOCK_CHAIN)
.setPhase(DEPOSIT_UNLOCKED)
.setDepositPublished(true)
.setDepositConfirmed(true);
verifyExpectedProtocolStatus(trade);
logTrade(log,
testInfo,
userName + "'s view after deposit is confirmed",
trade);
break;
}
}
}
protected final void verifyTakerDepositConfirmed(TradeInfo trade) {
if (!trade.getIsDepositUnlocked()) {
fail(format("INVALID_PHASE for trade %s in STATE=%s PHASE=%s, deposit tx never unlocked.",
trade.getShortId(),
trade.getState(),
trade.getPhase()));
}
}
protected final void waitForBuyerSeesPaymentInitiatedMessage(Logger log,
TestInfo testInfo,
GrpcClient grpcClient,
String tradeId) {
String userName = toUserName.apply(grpcClient);
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
TradeInfo trade = grpcClient.getTrade(tradeId);
if (!trade.getIsPaymentSent()) {
log.warn("{} still waiting for trade {} {}, attempt # {}",
userName,
trade.getShortId(),
BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG,
i);
sleep(5_000);
} else {
// Do not check trade.getOffer().getState() here because
// it might be AVAILABLE, not OFFER_FEE_RESERVED.
EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG)
.setPhase(PAYMENT_SENT)
.setPaymentStartedMessageSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, userName + "'s view after confirming trade payment sent", trade);
break;
}
}
}
protected final void waitForSellerSeesPaymentInitiatedMessage(Logger log,
TestInfo testInfo,
GrpcClient grpcClient,
String tradeId) {
Predicate<TradeInfo> isTradeInPaymentReceiptConfirmedStateAndPhase = (t) ->
t.getState().equals(SELLER_RECEIVED_PAYMENT_SENT_MSG.name()) &&
(t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(PAYMENT_SENT.name()));
String userName = toUserName.apply(grpcClient);
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
TradeInfo trade = grpcClient.getTrade(tradeId);
if (!isTradeInPaymentReceiptConfirmedStateAndPhase.test(trade)) {
log.warn("INVALID_PHASE for {}'s trade {} in STATE={} PHASE={}, cannot confirm payment received yet.",
userName,
trade.getShortId(),
trade.getState(),
trade.getPhase());
sleep(10_000);
} else {
break;
}
}
TradeInfo trade = grpcClient.getTrade(tradeId);
if (!isTradeInPaymentReceiptConfirmedStateAndPhase.test(trade)) {
fail(format("INVALID_PHASE for %s's trade %s in STATE=%s PHASE=%s, cannot confirm payment received.",
userName,
trade.getShortId(),
trade.getState(),
trade.getPhase()));
}
} }
protected final void verifyExpectedProtocolStatus(TradeInfo trade) { protected final void verifyExpectedProtocolStatus(TradeInfo trade) {
@ -49,35 +184,54 @@ public class AbstractTradeTest extends AbstractOfferTest {
if (!isLongRunningTest) if (!isLongRunningTest)
assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositPublished, trade.getIsDepositPublished()); assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositPublished, trade.getIsDepositPublished());
assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositUnlocked, trade.getIsDepositUnlocked()); assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositConfirmed, trade.getIsDepositUnlocked());
assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentSent, trade.getIsPaymentSent()); assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentStartedMessageSent, trade.getIsPaymentSent());
assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentReceived, trade.getIsPaymentReceived()); assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentReceivedMessageSent, trade.getIsPaymentReceived());
assertEquals(EXPECTED_PROTOCOL_STATUS.isPayoutPublished, trade.getIsPayoutPublished()); assertEquals(EXPECTED_PROTOCOL_STATUS.isPayoutPublished, trade.getIsPayoutPublished());
assertEquals(EXPECTED_PROTOCOL_STATUS.isWithdrawn, trade.getIsWithdrawn()); assertEquals(EXPECTED_PROTOCOL_STATUS.isCompleted, trade.getIsCompleted());
}
protected final void logBalances(Logger log, TestInfo testInfo) {
var alicesBalances = aliceClient.getBalances();
log.debug("{} Alice's Current Balances:\n{}",
testName(testInfo),
formatBalancesTbls(alicesBalances));
var bobsBalances = bobClient.getBalances();
log.debug("{} Bob's Current Balances:\n{}",
testName(testInfo),
formatBalancesTbls(bobsBalances));
} }
protected final void logTrade(Logger log, protected final void logTrade(Logger log,
TestInfo testInfo, TestInfo testInfo,
String description, String description,
TradeInfo trade) { TradeInfo trade) {
logTrade(log, testInfo, description, trade, false); if (log.isDebugEnabled()) {
} log.debug(format("%s %s%n%s",
protected final void logTrade(Logger log,
TestInfo testInfo,
String description,
TradeInfo trade,
boolean force) {
if (force)
log.info(String.format("%s %s%n%s",
testName(testInfo), testName(testInfo),
description.toUpperCase(), description,
format(trade))); new TableBuilder(TRADE_DETAIL_TBL, trade).build()));
else if (log.isDebugEnabled()) {
log.debug(String.format("%s %s%n%s",
testName(testInfo),
description.toUpperCase(),
format(trade)));
} }
} }
protected static void runCliGetTrade(String tradeId) {
out.println("Alice's CLI 'gettrade' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9998", "gettrade", "--trade-id=" + tradeId});
out.println("Bob's CLI 'gettrade' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrade", "--trade-id=" + tradeId});
}
protected static void runCliGetOpenTrades() {
out.println("Alice's CLI 'gettrades --category=open' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9998", "gettrades", "--category=open"});
out.println("Bob's CLI 'gettrades --category=open' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrades", "--category=open"});
}
protected static void runCliGetClosedTrades() {
out.println("Alice's CLI 'gettrades --category=closed' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9998", "gettrades", "--category=closed"});
out.println("Bob's CLI 'gettrades --category=closed' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrades", "--category=closed"});
}
} }

View file

@ -10,11 +10,11 @@ public class ExpectedProtocolStatus {
Trade.State state; Trade.State state;
Trade.Phase phase; Trade.Phase phase;
boolean isDepositPublished; boolean isDepositPublished;
boolean isDepositUnlocked; boolean isDepositConfirmed;
boolean isPaymentSent; boolean isPaymentStartedMessageSent;
boolean isPaymentReceived; boolean isPaymentReceivedMessageSent;
boolean isPayoutPublished; boolean isPayoutPublished;
boolean isWithdrawn; boolean isCompleted;
public ExpectedProtocolStatus setState(Trade.State state) { public ExpectedProtocolStatus setState(Trade.State state) {
this.state = state; this.state = state;
@ -31,18 +31,18 @@ public class ExpectedProtocolStatus {
return this; return this;
} }
public ExpectedProtocolStatus setDepositUnlocked(boolean depositUnlocked) { public ExpectedProtocolStatus setDepositConfirmed(boolean depositConfirmed) {
isDepositUnlocked = depositUnlocked; isDepositConfirmed = depositConfirmed;
return this; return this;
} }
public ExpectedProtocolStatus setFiatSent(boolean paymentSent) { public ExpectedProtocolStatus setPaymentStartedMessageSent(boolean paymentStartedMessageSent) {
isPaymentSent = paymentSent; isPaymentStartedMessageSent = paymentStartedMessageSent;
return this; return this;
} }
public ExpectedProtocolStatus setFiatReceived(boolean paymentReceived) { public ExpectedProtocolStatus setPaymentReceivedMessageSent(boolean paymentReceivedMessageSent) {
isPaymentReceived = paymentReceived; isPaymentReceivedMessageSent = paymentReceivedMessageSent;
return this; return this;
} }
@ -51,8 +51,8 @@ public class ExpectedProtocolStatus {
return this; return this;
} }
public ExpectedProtocolStatus setWithdrawn(boolean withdrawn) { public ExpectedProtocolStatus setCompleted(boolean completed) {
isWithdrawn = withdrawn; isCompleted = completed;
return this; return this;
} }
@ -60,10 +60,10 @@ public class ExpectedProtocolStatus {
state = null; state = null;
phase = null; phase = null;
isDepositPublished = false; isDepositPublished = false;
isDepositUnlocked = false; isDepositConfirmed = false;
isPaymentSent = false; isPaymentStartedMessageSent = false;
isPaymentReceived = false; isPaymentReceivedMessageSent = false;
isPayoutPublished = false; isPayoutPublished = false;
isWithdrawn = false; isCompleted = false;
} }
} }

View file

@ -19,12 +19,8 @@ package bisq.apitest.method.trade;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.TradeInfo;
import io.grpc.StatusRuntimeException; import io.grpc.StatusRuntimeException;
import java.util.function.Predicate;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
@ -34,18 +30,15 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import static bisq.cli.TableFormat.formatBalancesTbls; import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; import static bisq.apitest.config.ApiTestConfig.USD;
import static bisq.core.trade.Trade.Phase.DEPOSIT_UNLOCKED;
import static bisq.core.trade.Trade.Phase.PAYMENT_SENT;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
import static bisq.core.trade.Trade.State.*; import static bisq.core.trade.Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG;
import static java.lang.String.format; import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_RESERVED; import static protobuf.OfferDirection.BUY;
import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OpenOffer.State.AVAILABLE; import static protobuf.OpenOffer.State.AVAILABLE;
@Disabled @Disabled
@ -61,62 +54,35 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
try { try {
PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US"); PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US");
var alicesOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), var alicesOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
"usd", USD,
12_500_000L, 12_500_000L,
12_500_000L, // min-amount = amount 12_500_000L, // min-amount = amount
0.00, 0.00,
getDefaultBuyerSecurityDepositAsPercent(), defaultBuyerSecurityDepositPct.get(),
alicesUsdAccount.getId()); alicesUsdAccount.getId(),
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId(); var offerId = alicesOffer.getId();
// Wait for Alice's AddToOfferBook task. // Wait for Alice's AddToOfferBook task.
// Wait times vary; my logs show >= 2 second delay. // Wait times vary; my logs show >= 2-second delay.
sleep(3000); // TODO loop instead of hard code wait time sleep(3_000); // TODO loop instead of hard code a wait time
var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), "usd"); var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), USD);
assertEquals(1, alicesUsdOffers.size()); assertEquals(1, alicesUsdOffers.size());
PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US"); PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US");
var trade = takeAlicesOffer(offerId, bobsUsdAccount.getId()); var trade = takeAlicesOffer(offerId,
assertNotNull(trade); bobsUsdAccount.getId(),
assertEquals(offerId, trade.getTradeId()); false);
// Cache the trade id for the other tests. sleep(2_500); // Allow available offer to be removed from offer book.
tradeId = trade.getTradeId(); alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), USD);
genBtcBlocksThenWait(1, 4000);
alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), "usd");
assertEquals(0, alicesUsdOffers.size()); assertEquals(0, alicesUsdOffers.size());
genBtcBlocksThenWait(1, 2_500);
waitForDepositConfirmation(log, testInfo, bobClient, trade.getTradeId());
genBtcBlocksThenWait(1, 2500); trade = bobClient.getTrade(tradeId);
verifyTakerDepositConfirmed(trade);
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId));
trade = bobClient.getTrade(trade.getTradeId()); logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId));
if (!trade.getIsDepositUnlocked()) {
log.warn("Bob still waiting on trade {} maker tx {} taker tx {}: DEPOSIT_UNLOCKED_IN_BLOCK_CHAIN, attempt # {}",
trade.getShortId(),
trade.getMakerDepositTxId(),
trade.getTakerDepositTxId(),
i);
genBtcBlocksThenWait(1, 4000);
continue;
} else {
EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_UNLOCKED_IN_BLOCK_CHAIN)
.setPhase(DEPOSIT_UNLOCKED)
.setDepositPublished(true)
.setDepositUnlocked(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after deposit is unlocked", trade, true);
break;
}
}
if (!trade.getIsDepositUnlocked()) {
fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, deposit tx never unlocked.",
trade.getShortId(),
trade.getState(),
trade.getPhase()));
}
} catch (StatusRuntimeException e) { } catch (StatusRuntimeException e) {
fail(e); fail(e);
} }
@ -127,56 +93,10 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) { public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) {
try { try {
var trade = aliceClient.getTrade(tradeId); var trade = aliceClient.getTrade(tradeId);
waitForDepositConfirmation(log, testInfo, aliceClient, trade.getTradeId());
Predicate<TradeInfo> tradeStateAndPhaseCorrect = (t) ->
t.getState().equals(DEPOSIT_UNLOCKED_IN_BLOCK_CHAIN.name())
&& t.getPhase().equals(DEPOSIT_UNLOCKED.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()); aliceClient.confirmPaymentStarted(trade.getTradeId());
sleep(6000); sleep(6_000);
waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId);
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
trade = aliceClient.getTrade(tradeId);
if (!trade.getIsPaymentSent()) {
log.warn("Alice still waiting for trade {} BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG, attempt # {}",
trade.getShortId(),
i);
sleep(5000);
continue;
} else {
assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG)
.setPhase(PAYMENT_SENT)
.setFiatSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Alice's view after confirming fiat payment sent", trade);
break;
}
}
} catch (StatusRuntimeException e) { } catch (StatusRuntimeException e) {
fail(e); fail(e);
} }
@ -186,82 +106,19 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
@Order(3) @Order(3)
public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { public void testBobsConfirmPaymentReceived(final TestInfo testInfo) {
try { try {
waitForSellerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId);
var trade = bobClient.getTrade(tradeId); var trade = bobClient.getTrade(tradeId);
Predicate<TradeInfo> tradeStateAndPhaseCorrect = (t) ->
t.getState().equals(SELLER_RECEIVED_PAYMENT_SENT_MSG.name())
&& (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(PAYMENT_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()); bobClient.confirmPaymentReceived(trade.getTradeId());
sleep(3000); sleep(3_000);
trade = bobClient.getTrade(tradeId); trade = bobClient.getTrade(tradeId);
// Note: offer.state == available // Note: offer.state == available
assertEquals(AVAILABLE.name(), trade.getOffer().getState()); assertEquals(AVAILABLE.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG)
.setPhase(PAYOUT_PUBLISHED) .setPhase(PAYOUT_PUBLISHED)
.setPayoutPublished(true) .setPayoutPublished(true)
.setFiatReceived(true); .setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade); verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after confirming fiat payment received", trade); logTrade(log, testInfo, "Bob's view after confirming fiat payment received", trade);
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(4)
public void testAlicesKeepFunds(final TestInfo testInfo) {
try {
genBtcBlocksThenWait(1, 1000);
var trade = aliceClient.getTrade(tradeId);
logTrade(log, testInfo, "Alice's view before keeping funds", trade);
aliceClient.keepFunds(tradeId);
genBtcBlocksThenWait(1, 1000);
trade = aliceClient.getTrade(tradeId);
EXPECTED_PROTOCOL_STATUS.setState(BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG)
.setPhase(PAYOUT_PUBLISHED);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Alice's view after keeping funds", trade);
logTrade(log, testInfo, "Alice's Maker/Buyer View (Done)", aliceClient.getTrade(tradeId), true);
logTrade(log, testInfo, "Bob's Taker/Seller View (Done)", bobClient.getTrade(tradeId), true);
var alicesBalances = aliceClient.getBalances();
log.info("{} Alice's Current Balance:\n{}",
testName(testInfo),
formatBalancesTbls(alicesBalances));
var bobsBalances = bobClient.getBalances();
log.info("{} Bob's Current Balance:\n{}",
testName(testInfo),
formatBalancesTbls(bobsBalances));
} catch (StatusRuntimeException e) { } catch (StatusRuntimeException e) {
fail(e); fail(e);
} }

View file

@ -0,0 +1,253 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.method.trade;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.NationalBankAccountPayload;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG;
import static org.junit.jupiter.api.Assertions.*;
import static protobuf.Offer.State.OFFER_FEE_RESERVED;
import static protobuf.OfferDirection.BUY;
import static protobuf.OpenOffer.State.AVAILABLE;
/**
* Test case verifies trade can be made with national bank payment method,
* and json contracts exclude bank acct details until deposit tx is confirmed.
*/
@SuppressWarnings("ConstantConditions")
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TakeBuyBTCOfferWithNationalBankAcctTest extends AbstractTradeTest {
// Alice is maker/buyer, Bob is taker/seller.
private static final String BRL = "BRL";
private static PaymentAccount alicesPaymentAccount;
private static PaymentAccount bobsPaymentAccount;
@BeforeAll
public static void setUp() {
setUp(false);
}
@Test
@Order(1)
public void testTakeAlicesBuyOffer(final TestInfo testInfo) {
try {
alicesPaymentAccount = createDummyBRLAccount(aliceClient,
"Alicia da Silva",
String.valueOf(System.currentTimeMillis()),
"123.456.789-01");
bobsPaymentAccount = createDummyBRLAccount(bobClient,
"Roberto da Silva",
String.valueOf(System.currentTimeMillis()),
"123.456.789-02");
var alicesOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
BRL,
1_000_000L,
1_000_000L, // min-amount = amount
0.00,
defaultBuyerSecurityDepositPct.get(),
alicesPaymentAccount.getId(),
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId();
// Wait for Alice's AddToOfferBook task.
// Wait times vary; my logs show >= 2 second delay.
sleep(3_000); // TODO loop instead of hard code wait time
var alicesOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), BRL);
assertEquals(1, alicesOffers.size());
var trade = takeAlicesOffer(offerId,
bobsPaymentAccount.getId(),
false);
// Before generating a blk and confirming deposit tx, make sure there
// are no bank acct details in the either side's contract.
while (true) {
try {
var alicesContract = aliceClient.getTrade(trade.getTradeId()).getContractAsJson();
var bobsContract = bobClient.getTrade(trade.getTradeId()).getContractAsJson();
verifyJsonContractExcludesBankAccountDetails(alicesContract, alicesPaymentAccount);
verifyJsonContractExcludesBankAccountDetails(alicesContract, bobsPaymentAccount);
verifyJsonContractExcludesBankAccountDetails(bobsContract, alicesPaymentAccount);
verifyJsonContractExcludesBankAccountDetails(bobsContract, bobsPaymentAccount);
break;
} catch (StatusRuntimeException ex) {
if (ex.getMessage() == null) {
String message = ex.getMessage().replaceFirst("^[A-Z_]+: ", "");
if (message.contains("trade") && message.contains("not found")) {
fail(ex);
}
} else {
sleep(500);
}
}
}
genBtcBlocksThenWait(1, 4000);
alicesOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), BRL);
assertEquals(0, alicesOffers.size());
genBtcBlocksThenWait(1, 2_500);
waitForDepositConfirmation(log, testInfo, bobClient, trade.getTradeId());
trade = bobClient.getTrade(tradeId);
verifyTakerDepositConfirmed(trade);
logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(2)
public void testBankAcctDetailsIncludedInContracts(final TestInfo testInfo) {
assertNotNull(alicesPaymentAccount);
assertNotNull(bobsPaymentAccount);
var alicesTrade = aliceClient.getTrade(tradeId);
assertNotEquals("", alicesTrade.getContract().getMakerPaymentAccountPayload().getPaymentDetails());
assertNotEquals("", alicesTrade.getContract().getTakerPaymentAccountPayload().getPaymentDetails());
var alicesContractJson = alicesTrade.getContractAsJson();
verifyJsonContractIncludesBankAccountDetails(alicesContractJson, alicesPaymentAccount);
verifyJsonContractIncludesBankAccountDetails(alicesContractJson, bobsPaymentAccount);
var bobsTrade = bobClient.getTrade(tradeId);
assertNotEquals("", bobsTrade.getContract().getMakerPaymentAccountPayload().getPaymentDetails());
assertNotEquals("", bobsTrade.getContract().getTakerPaymentAccountPayload().getPaymentDetails());
var bobsContractJson = bobsTrade.getContractAsJson();
verifyJsonContractIncludesBankAccountDetails(bobsContractJson, alicesPaymentAccount);
verifyJsonContractIncludesBankAccountDetails(bobsContractJson, bobsPaymentAccount);
}
@Test
@Order(3)
public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) {
try {
var trade = aliceClient.getTrade(tradeId);
waitForDepositConfirmation(log, testInfo, aliceClient, trade.getTradeId());
aliceClient.confirmPaymentStarted(trade.getTradeId());
sleep(6_000);
waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId);
trade = aliceClient.getTrade(tradeId);
assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState());
logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Sent)", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Sent)", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(4)
public void testBobsConfirmPaymentReceived(final TestInfo testInfo) {
try {
waitForSellerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId);
var trade = bobClient.getTrade(tradeId);
bobClient.confirmPaymentReceived(trade.getTradeId());
sleep(3_000);
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)
.setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after confirming fiat payment received", trade);
} catch (StatusRuntimeException e) {
fail(e);
}
}
private void verifyJsonContractExcludesBankAccountDetails(String jsonContract,
PaymentAccount paymentAccount) {
NationalBankAccountPayload nationalBankAccountPayload =
(NationalBankAccountPayload) paymentAccount.getPaymentAccountPayload();
// The client cannot know exactly when payment acct payloads are added to a contract,
// so auto-failing here results in a flaky test.
// assertFalse(jsonContract.contains(nationalBankAccountPayload.getNationalAccountId()));
// assertFalse(jsonContract.contains(nationalBankAccountPayload.getBranchId()));
// assertFalse(jsonContract.contains(nationalBankAccountPayload.getAccountNr()));
// assertFalse(jsonContract.contains(nationalBankAccountPayload.getHolderName()));
// assertFalse(jsonContract.contains(nationalBankAccountPayload.getHolderTaxId()));
// Log warning if bank acct details are found in json contract.
if (jsonContract.contains(nationalBankAccountPayload.getNationalAccountId()))
log.warn("Could not check json contract soon enough; it contains national bank acct id");
if (jsonContract.contains(nationalBankAccountPayload.getBranchId()))
log.warn("Could not check json contract soon enough; it contains natl bank branch id");
if (jsonContract.contains(nationalBankAccountPayload.getAccountNr()))
log.warn("Could not check json contract soon enough; it contains natl bank acct #");
if (jsonContract.contains(nationalBankAccountPayload.getHolderName()))
log.warn("Could not check json contract soon enough; it contains natl bank acct holder name");
if (jsonContract.contains(nationalBankAccountPayload.getHolderTaxId()))
log.warn("Could not check json contract soon enough; it contains natl bank acct holder tax id");
}
private void verifyJsonContractIncludesBankAccountDetails(String jsonContract,
PaymentAccount paymentAccount) {
NationalBankAccountPayload nationalBankAccountPayload =
(NationalBankAccountPayload) paymentAccount.getPaymentAccountPayload();
assertTrue(jsonContract.contains(nationalBankAccountPayload.getNationalAccountId()));
assertTrue(jsonContract.contains(nationalBankAccountPayload.getBranchId()));
assertTrue(jsonContract.contains(nationalBankAccountPayload.getAccountNr()));
assertTrue(jsonContract.contains(nationalBankAccountPayload.getHolderName()));
assertTrue(jsonContract.contains(nationalBankAccountPayload.getHolderTaxId()));
}
}

View file

@ -0,0 +1,144 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.method.trade;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.apitest.config.ApiTestConfig.XMR;
import static bisq.cli.table.builder.TableType.OFFER_TBL;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_RESERVED;
import static protobuf.OfferDirection.SELL;
import bisq.apitest.method.offer.AbstractOfferTest;
import bisq.cli.table.builder.TableBuilder;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TakeBuyXMROfferTest extends AbstractTradeTest {
// Alice is maker / xmr buyer (btc seller), Bob is taker / xmr seller (btc buyer).
@BeforeAll
public static void setUp() {
AbstractOfferTest.setUp();
createXmrPaymentAccounts();
EXPECTED_PROTOCOL_STATUS.init();
}
@Test
@Order(1)
public void testTakeAlicesSellBTCForXMROffer(final TestInfo testInfo) {
try {
// Alice is going to BUY XMR, but the Offer direction = SELL because it is a
// BTC trade; Alice will SELL BTC for XMR. Bob will send Alice XMR.
// Confused me, but just need to remember there are only BTC offers.
var btcTradeDirection = SELL.name();
var alicesOffer = aliceClient.createFixedPricedOffer(btcTradeDirection,
XMR,
15_000_000L,
7_500_000L,
"0.00455500", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
defaultBuyerSecurityDepositPct.get(),
alicesXmrAcct.getId());
log.debug("Alice's BUY XMR (SELL BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());
genBtcBlocksThenWait(1, 5000);
var offerId = alicesOffer.getId();
var alicesXmrOffers = aliceClient.getMyOffers(btcTradeDirection, XMR);
assertEquals(1, alicesXmrOffers.size());
var trade = takeAlicesOffer(offerId, bobsXmrAcct.getId());
alicesXmrOffers = aliceClient.getMyOffersSortedByDate(XMR);
assertEquals(0, alicesXmrOffers.size());
genBtcBlocksThenWait(1, 2_500);
waitForDepositConfirmation(log, testInfo, bobClient, trade.getTradeId());
trade = bobClient.getTrade(tradeId);
verifyTakerDepositConfirmed(trade);
logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(2)
public void testBobsConfirmPaymentStarted(final TestInfo testInfo) {
try {
var trade = bobClient.getTrade(tradeId);
verifyTakerDepositConfirmed(trade);
log.debug("Bob sends XMR payment to Alice for trade {}", trade.getTradeId());
bobClient.confirmPaymentStarted(trade.getTradeId());
sleep(3500);
waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId);
logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Sent)", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Sent)", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(3)
public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) {
try {
waitForSellerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId);
sleep(2_000);
var trade = aliceClient.getTrade(tradeId);
// If we were trading BSQ, Alice would verify payment has been sent to her
// Bisq wallet, but we can do no such checks for XMR payments.
// All XMR transfers are done outside Bisq.
log.debug("Alice verifies XMR payment was received from Bob, for trade {}", trade.getTradeId());
aliceClient.confirmPaymentReceived(trade.getTradeId());
sleep(3_000);
trade = aliceClient.getTrade(tradeId);
assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG)
.setPhase(PAYOUT_PUBLISHED)
.setPayoutPublished(true)
.setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Received)", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Received)", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
}

View file

@ -19,12 +19,8 @@ package bisq.apitest.method.trade;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.TradeInfo;
import io.grpc.StatusRuntimeException; import io.grpc.StatusRuntimeException;
import java.util.function.Predicate;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
@ -35,20 +31,16 @@ import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BTC; import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.cli.TableFormat.formatBalancesTbls; import static bisq.apitest.config.ApiTestConfig.USD;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static bisq.core.trade.Trade.Phase.DEPOSIT_UNLOCKED;
import static bisq.core.trade.Trade.Phase.PAYMENT_SENT;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
import static bisq.core.trade.Trade.Phase.WITHDRAWN; import static bisq.core.trade.Trade.Phase.WITHDRAWN;
import static bisq.core.trade.Trade.State.*; import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG;
import static java.lang.String.format; import static bisq.core.trade.Trade.State.WITHDRAW_COMPLETED;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_RESERVED; import static protobuf.Offer.State.OFFER_FEE_RESERVED;
import static protobuf.OfferPayload.Direction.SELL; import static protobuf.OfferDirection.SELL;
import static protobuf.OpenOffer.State.AVAILABLE;
@Disabled @Disabled
@Slf4j @Slf4j
@ -57,6 +49,9 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
// Alice is maker/seller, Bob is taker/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;
private static final String WITHDRAWAL_TX_MEMO = "Bob's trade withdrawal"; private static final String WITHDRAWAL_TX_MEMO = "Bob's trade withdrawal";
@Test @Test
@ -65,63 +60,35 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
try { try {
PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US"); PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US");
var alicesOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(), var alicesOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(),
"usd", USD,
12_500_000L, 12_500_000L,
12_500_000L, // min-amount = amount 12_500_000L, // min-amount = amount
0.00, 0.00,
getDefaultBuyerSecurityDepositAsPercent(), defaultBuyerSecurityDepositPct.get(),
alicesUsdAccount.getId()); alicesUsdAccount.getId(),
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId(); var offerId = alicesOffer.getId();
// Wait for Alice's AddToOfferBook task. // Wait for Alice's AddToOfferBook task.
// Wait times vary; my logs show >= 2 second delay, but taking sell offers // Wait times vary; my logs show >= 2-second delay, but taking sell offers
// seems to require more time to prepare. // seems to require more time to prepare.
sleep(3000); // TODO loop instead of hard code wait time sleep(3_000); // TODO loop instead of hard code a wait time
var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(SELL.name(), "usd"); var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(SELL.name(), USD);
assertEquals(1, alicesUsdOffers.size()); assertEquals(1, alicesUsdOffers.size());
PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US"); PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US");
var trade = takeAlicesOffer(offerId, bobsUsdAccount.getId()); var trade = takeAlicesOffer(offerId,
assertNotNull(trade); bobsUsdAccount.getId(),
assertEquals(offerId, trade.getTradeId()); false);
// Cache the trade id for the other tests. sleep(2_500); // Allow available offer to be removed from offer book.
tradeId = trade.getTradeId(); var takeableUsdOffers = bobClient.getOffersSortedByDate(SELL.name(), USD);
genBtcBlocksThenWait(1, 4000);
var takeableUsdOffers = bobClient.getOffersSortedByDate(SELL.name(), "usd");
assertEquals(0, takeableUsdOffers.size()); assertEquals(0, takeableUsdOffers.size());
genBtcBlocksThenWait(1, 2_500);
genBtcBlocksThenWait(1, 2500); waitForDepositConfirmation(log, testInfo, bobClient, trade.getTradeId());
trade = bobClient.getTrade(tradeId);
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { verifyTakerDepositConfirmed(trade);
trade = bobClient.getTrade(trade.getTradeId()); logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId));
if (!trade.getIsDepositUnlocked()) {
log.warn("Bob still waiting on trade {} maker tx {} taker tx {}: DEPOSIT_UNLOCKED_IN_BLOCK_CHAIN, attempt # {}",
trade.getShortId(),
trade.getMakerDepositTxId(),
trade.getTakerDepositTxId(),
i);
genBtcBlocksThenWait(1, 4000);
continue;
} else {
EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_UNLOCKED_IN_BLOCK_CHAIN)
.setPhase(DEPOSIT_UNLOCKED)
.setDepositPublished(true)
.setDepositUnlocked(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after deposit is confirmed", trade, true);
break;
}
}
if (!trade.getIsDepositUnlocked()) {
fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, deposit tx never unlocked.",
trade.getShortId(),
trade.getState(),
trade.getPhase()));
}
} catch (StatusRuntimeException e) { } catch (StatusRuntimeException e) {
fail(e); fail(e);
} }
@ -132,54 +99,10 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
public void testBobsConfirmPaymentStarted(final TestInfo testInfo) { public void testBobsConfirmPaymentStarted(final TestInfo testInfo) {
try { try {
var trade = bobClient.getTrade(tradeId); var trade = bobClient.getTrade(tradeId);
verifyTakerDepositConfirmed(trade);
Predicate<TradeInfo> tradeStateAndPhaseCorrect = (t) ->
t.getState().equals(DEPOSIT_UNLOCKED_IN_BLOCK_CHAIN.name()) && t.getPhase().equals(DEPOSIT_UNLOCKED.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); bobClient.confirmPaymentStarted(tradeId);
sleep(6000); sleep(6_000);
waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId);
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
trade = bobClient.getTrade(tradeId);
if (!trade.getIsPaymentSent()) {
log.warn("Bob still waiting for trade {} BUYER_SAW_ARRIVED_PAYMENT_SENT_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_PAYMENT_SENT_MSG)
.setPhase(PAYMENT_SENT)
.setFiatSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after confirming fiat payment sent", trade);
break;
}
}
} catch (StatusRuntimeException e) { } catch (StatusRuntimeException e) {
fail(e); fail(e);
} }
@ -189,83 +112,21 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
@Order(3) @Order(3)
public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) { public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) {
try { try {
waitForSellerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId);
var trade = aliceClient.getTrade(tradeId); var trade = aliceClient.getTrade(tradeId);
Predicate<TradeInfo> tradeStateAndPhaseCorrect = (t) ->
t.getState().equals(SELLER_RECEIVED_PAYMENT_SENT_MSG.name())
&& (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(PAYMENT_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()); aliceClient.confirmPaymentReceived(trade.getTradeId());
sleep(3000); sleep(3_000);
trade = aliceClient.getTrade(tradeId); trade = aliceClient.getTrade(tradeId);
assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState()); assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG)
.setPhase(PAYOUT_PUBLISHED) .setPhase(PAYOUT_PUBLISHED)
.setPayoutPublished(true) .setPayoutPublished(true)
.setFiatReceived(true); .setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade); verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade); logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade);
} catch (StatusRuntimeException e) { } catch (StatusRuntimeException e) {
fail(e); fail(e);
} }
} }
@Test
@Order(4)
public void testBobsBtcWithdrawalToExternalAddress(final TestInfo testInfo) {
try {
genBtcBlocksThenWait(1, 1000);
var trade = bobClient.getTrade(tradeId);
logTrade(log, testInfo, "Bob's view before withdrawing funds to external wallet", trade);
String toAddress = bitcoinCli.getNewBtcAddress();
bobClient.withdrawFunds(tradeId, toAddress, WITHDRAWAL_TX_MEMO);
genBtcBlocksThenWait(1, 1000);
trade = bobClient.getTrade(tradeId);
EXPECTED_PROTOCOL_STATUS.setState(WITHDRAW_COMPLETED)
.setPhase(WITHDRAWN)
.setWithdrawn(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after withdrawing BTC funds to external wallet", trade);
logTrade(log, testInfo, "Alice's Maker/Buyer View (Done)", aliceClient.getTrade(tradeId), true);
logTrade(log, testInfo, "Bob's Taker/Seller View (Done)", bobClient.getTrade(tradeId), true);
var alicesBalances = aliceClient.getBalances();
log.info("{} Alice's Current Balance:\n{}",
testName(testInfo),
formatBalancesTbls(alicesBalances));
var bobsBalances = bobClient.getBalances();
log.info("{} Bob's Current Balance:\n{}",
testName(testInfo),
formatBalancesTbls(bobsBalances));
} catch (StatusRuntimeException e) {
fail(e);
}
}
} }

View file

@ -0,0 +1,153 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.method.trade;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.apitest.config.ApiTestConfig.XMR;
import static bisq.cli.table.builder.TableType.OFFER_TBL;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
import static bisq.core.trade.Trade.Phase.WITHDRAWN;
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG;
import static bisq.core.trade.Trade.State.WITHDRAW_COMPLETED;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.OfferDirection.BUY;
import bisq.apitest.method.offer.AbstractOfferTest;
import bisq.cli.table.builder.TableBuilder;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TakeSellXMROfferTest extends AbstractTradeTest {
// Alice is maker / xmr seller (btc buyer), Bob is taker / xmr buyer (btc seller).
// Maker and Taker fees are in BTC.
private static final String TRADE_FEE_CURRENCY_CODE = BTC;
private static final String WITHDRAWAL_TX_MEMO = "Bob's trade withdrawal";
@BeforeAll
public static void setUp() {
AbstractOfferTest.setUp();
createXmrPaymentAccounts();
EXPECTED_PROTOCOL_STATUS.init();
}
@Test
@Order(1)
public void testTakeAlicesBuyBTCForXMROffer(final TestInfo testInfo) {
try {
// Alice is going to SELL XMR, but the Offer direction = BUY because it is a
// BTC trade; Alice will BUY BTC for XMR. Alice will send Bob XMR.
// Confused me, but just need to remember there are only BTC offers.
var btcTradeDirection = BUY.name();
double priceMarginPctInput = 1.50;
var alicesOffer = aliceClient.createMarketBasedPricedOffer(btcTradeDirection,
XMR,
20_000_000L,
10_500_000L,
priceMarginPctInput,
defaultBuyerSecurityDepositPct.get(),
alicesXmrAcct.getId(),
NO_TRIGGER_PRICE);
log.debug("Alice's SELL XMR (BUY BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());
genBtcBlocksThenWait(1, 4000);
var offerId = alicesOffer.getId();
var alicesXmrOffers = aliceClient.getMyOffers(btcTradeDirection, XMR);
assertEquals(1, alicesXmrOffers.size());
var trade = takeAlicesOffer(offerId, bobsXmrAcct.getId());
alicesXmrOffers = aliceClient.getMyOffersSortedByDate(XMR);
assertEquals(0, alicesXmrOffers.size());
genBtcBlocksThenWait(1, 2_500);
waitForDepositConfirmation(log, testInfo, bobClient, trade.getTradeId());
trade = bobClient.getTrade(tradeId);
verifyTakerDepositConfirmed(trade);
logTrade(log, testInfo, "Alice's Maker/Seller View", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Buyer View", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(2)
public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) {
try {
var trade = aliceClient.getTrade(tradeId);
waitForDepositConfirmation(log, testInfo, aliceClient, trade.getTradeId());
log.debug("Alice sends XMR payment to Bob for trade {}", trade.getTradeId());
aliceClient.confirmPaymentStarted(trade.getTradeId());
sleep(3500);
waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId);
logTrade(log, testInfo, "Alice's Maker/Seller View (Payment Sent)", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Buyer View (Payment Sent)", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(3)
public void testBobsConfirmPaymentReceived(final TestInfo testInfo) {
try {
waitForSellerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId);
var trade = bobClient.getTrade(tradeId);
sleep(2_000);
// If we were trading BTC, Bob would verify payment has been sent to his
// Bisq wallet, but we can do no such checks for XMR payments.
// All XMR transfers are done outside Bisq.
log.debug("Bob verifies XMR payment was received from Alice, for trade {}", trade.getTradeId());
bobClient.confirmPaymentReceived(trade.getTradeId());
sleep(3_000);
trade = bobClient.getTrade(tradeId);
// Warning: trade.getOffer().getState() might be AVAILABLE, not OFFER_FEE_RESERVED.
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG)
.setPhase(PAYOUT_PUBLISHED)
.setPayoutPublished(true)
.setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Alice's Maker/Seller View (Payment Received)", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Buyer View (Payment Received)", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
}

View file

@ -56,10 +56,9 @@ public class BtcTxFeeRateTest extends MethodTest {
@Order(2) @Order(2)
public void testSetInvalidTxFeeRateShouldThrowException(final TestInfo testInfo) { public void testSetInvalidTxFeeRateShouldThrowException(final TestInfo testInfo) {
var currentTxFeeRateInfo = TxFeeRateInfo.fromProto(aliceClient.getTxFeeRate()); var currentTxFeeRateInfo = TxFeeRateInfo.fromProto(aliceClient.getTxFeeRate());
Throwable exception = assertThrows(StatusRuntimeException.class, () -> Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.setTxFeeRate(1));
aliceClient.setTxFeeRate(10));
String expectedExceptionMessage = String expectedExceptionMessage =
format("UNKNOWN: tx fee rate preference must be >= %d sats/byte", format("INVALID_ARGUMENT: tx fee rate preference must be >= %d sats/byte",
currentTxFeeRateInfo.getMinFeeServiceRate()); currentTxFeeRateInfo.getMinFeeServiceRate());
assertEquals(expectedExceptionMessage, exception.getMessage()); assertEquals(expectedExceptionMessage, exception.getMessage());
} }

View file

@ -19,9 +19,8 @@ import static bisq.apitest.config.HavenoAppConfig.bobdaemon;
import static bisq.apitest.config.HavenoAppConfig.seednode; import static bisq.apitest.config.HavenoAppConfig.seednode;
import static bisq.apitest.method.wallet.WalletTestUtil.INITIAL_BTC_BALANCES; import static bisq.apitest.method.wallet.WalletTestUtil.INITIAL_BTC_BALANCES;
import static bisq.apitest.method.wallet.WalletTestUtil.verifyBtcBalances; import static bisq.apitest.method.wallet.WalletTestUtil.verifyBtcBalances;
import static bisq.cli.TableFormat.formatAddressBalanceTbl; import static bisq.cli.table.builder.TableType.ADDRESS_BALANCE_TBL;
import static bisq.cli.TableFormat.formatBtcBalanceInfoTbl; import static bisq.cli.table.builder.TableType.BTC_BALANCE_TBL;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -30,6 +29,7 @@ import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import bisq.apitest.method.MethodTest; import bisq.apitest.method.MethodTest;
import bisq.cli.table.builder.TableBuilder;
@Disabled @Disabled
@Slf4j @Slf4j
@ -54,10 +54,14 @@ public class BtcWalletTest extends MethodTest {
// Bob & Alice's regtest Bisq wallets were initialized with 10 BTC. // Bob & Alice's regtest Bisq wallets were initialized with 10 BTC.
BtcBalanceInfo alicesBalances = aliceClient.getBtcBalances(); BtcBalanceInfo alicesBalances = aliceClient.getBtcBalances();
log.debug("{} Alice's BTC Balances:\n{}", testName(testInfo), formatBtcBalanceInfoTbl(alicesBalances)); log.debug("{} Alice's BTC Balances:\n{}",
testName(testInfo),
new TableBuilder(BTC_BALANCE_TBL, alicesBalances).build());
BtcBalanceInfo bobsBalances = bobClient.getBtcBalances(); BtcBalanceInfo bobsBalances = bobClient.getBtcBalances();
log.debug("{} Bob's BTC Balances:\n{}", testName(testInfo), formatBtcBalanceInfoTbl(bobsBalances)); log.debug("{} Bob's BTC Balances:\n{}",
testName(testInfo),
new TableBuilder(BTC_BALANCE_TBL, bobsBalances).build());
assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), alicesBalances.getAvailableBalance()); assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), alicesBalances.getAvailableBalance());
assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), bobsBalances.getAvailableBalance()); assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), bobsBalances.getAvailableBalance());
@ -76,7 +80,8 @@ public class BtcWalletTest extends MethodTest {
log.debug("{} -> Alice's Funded Address Balance -> \n{}", log.debug("{} -> Alice's Funded Address Balance -> \n{}",
testName(testInfo), testName(testInfo),
formatAddressBalanceTbl(singletonList(aliceClient.getAddressBalance(newAddress)))); new TableBuilder(ADDRESS_BALANCE_TBL,
aliceClient.getAddressBalance(newAddress)));
// New balance is 12.5 BTC // New balance is 12.5 BTC
btcBalanceInfo = aliceClient.getBtcBalances(); btcBalanceInfo = aliceClient.getBtcBalances();
@ -88,7 +93,7 @@ public class BtcWalletTest extends MethodTest {
verifyBtcBalances(alicesExpectedBalances, btcBalanceInfo); verifyBtcBalances(alicesExpectedBalances, btcBalanceInfo);
log.debug("{} -> Alice's BTC Balances After Sending 2.5 BTC -> \n{}", log.debug("{} -> Alice's BTC Balances After Sending 2.5 BTC -> \n{}",
testName(testInfo), testName(testInfo),
formatBtcBalanceInfoTbl(btcBalanceInfo)); new TableBuilder(BTC_BALANCE_TBL, btcBalanceInfo).build());
} }
@Test @Test
@ -115,7 +120,7 @@ public class BtcWalletTest extends MethodTest {
BtcBalanceInfo alicesBalances = aliceClient.getBtcBalances(); BtcBalanceInfo alicesBalances = aliceClient.getBtcBalances();
log.debug("{} Alice's BTC Balances:\n{}", log.debug("{} Alice's BTC Balances:\n{}",
testName(testInfo), testName(testInfo),
formatBtcBalanceInfoTbl(alicesBalances)); new TableBuilder(BTC_BALANCE_TBL, alicesBalances).build());
bisq.core.api.model.BtcBalanceInfo alicesExpectedBalances = bisq.core.api.model.BtcBalanceInfo alicesExpectedBalances =
bisq.core.api.model.BtcBalanceInfo.valueOf(700000000, bisq.core.api.model.BtcBalanceInfo.valueOf(700000000,
0, 0,
@ -126,7 +131,7 @@ public class BtcWalletTest extends MethodTest {
BtcBalanceInfo bobsBalances = bobClient.getBtcBalances(); BtcBalanceInfo bobsBalances = bobClient.getBtcBalances();
log.debug("{} Bob's BTC Balances:\n{}", log.debug("{} Bob's BTC Balances:\n{}",
testName(testInfo), testName(testInfo),
formatBtcBalanceInfoTbl(bobsBalances)); new TableBuilder(BTC_BALANCE_TBL, bobsBalances).build());
// The sendbtc tx weight and size randomly varies between two distinct values // The sendbtc tx weight and size randomly varies between two distinct values
// (876 wu, 219 bytes, OR 880 wu, 220 bytes) from test run to test run, hence // (876 wu, 219 bytes, OR 880 wu, 220 bytes) from test run to test run, hence
// the assertion of an available balance range [1549978000, 1549978100]. // the assertion of an available balance range [1549978000, 1549978100].

View file

@ -48,7 +48,7 @@ public class WalletProtectionTest extends MethodTest {
@Order(2) @Order(2)
public void testGetBalanceOnEncryptedWalletShouldThrowException() { public void testGetBalanceOnEncryptedWalletShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances()); Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage());
} }
@Test @Test
@ -58,7 +58,7 @@ public class WalletProtectionTest extends MethodTest {
aliceClient.getBtcBalances(); // should not throw 'wallet locked' exception aliceClient.getBtcBalances(); // should not throw 'wallet locked' exception
sleep(4500); // let unlock timeout expire sleep(4500); // let unlock timeout expire
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances()); Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage());
} }
@Test @Test
@ -67,7 +67,7 @@ public class WalletProtectionTest extends MethodTest {
aliceClient.unlockWallet("first-password", 3); aliceClient.unlockWallet("first-password", 3);
sleep(4000); // let unlock timeout expire sleep(4000); // let unlock timeout expire
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances()); Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage());
} }
@Test @Test
@ -76,14 +76,14 @@ public class WalletProtectionTest extends MethodTest {
aliceClient.unlockWallet("first-password", 60); aliceClient.unlockWallet("first-password", 60);
aliceClient.lockWallet(); aliceClient.lockWallet();
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances()); Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage());
} }
@Test @Test
@Order(6) @Order(6)
public void testLockWalletWhenWalletAlreadyLockedShouldThrowException() { public void testLockWalletWhenWalletAlreadyLockedShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.lockWallet()); Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.lockWallet());
assertEquals("UNKNOWN: wallet is already locked", exception.getMessage()); assertEquals("ALREADY_EXISTS: wallet is already locked", exception.getMessage());
} }
@Test @Test
@ -110,7 +110,7 @@ public class WalletProtectionTest extends MethodTest {
public void testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException() { public void testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () -> Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceClient.setWalletPassword("bad old password", "irrelevant")); aliceClient.setWalletPassword("bad old password", "irrelevant"));
assertEquals("UNKNOWN: incorrect old password", exception.getMessage()); assertEquals("INVALID_ARGUMENT: incorrect old password", exception.getMessage());
} }
@Test @Test

View file

@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
@Slf4j @Slf4j
public class WalletTestUtil { public class WalletTestUtil {
// All api tests depend on the regtest environment, and Bob & Alice's wallets // All api tests depend on the DAO / regtest environment, and Bob & Alice's wallets
// are initialized with 10 BTC during the scaffolding setup. // are initialized with 10 BTC during the scaffolding setup.
public static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES = public static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES =
bisq.core.api.model.BtcBalanceInfo.valueOf(1000000000, bisq.core.api.model.BtcBalanceInfo.valueOf(1000000000,
@ -17,7 +17,6 @@ public class WalletTestUtil {
1000000000, 1000000000,
0); 0);
public static void verifyBtcBalances(bisq.core.api.model.BtcBalanceInfo expected, public static void verifyBtcBalances(bisq.core.api.model.BtcBalanceInfo expected,
BtcBalanceInfo actual) { BtcBalanceInfo actual) {
assertEquals(expected.getAvailableBalance(), actual.getAvailableBalance()); assertEquals(expected.getAvailableBalance(), actual.getAvailableBalance());

View file

@ -0,0 +1,161 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.OfferInfo;
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 static org.junit.jupiter.api.Assertions.fail;
import static protobuf.OfferDirection.BUY;
import static protobuf.OfferDirection.SELL;
import bisq.apitest.method.offer.AbstractOfferTest;
/**
* Used to verify trigger based, automatic offer deactivation works.
* Disabled by default.
* Set ENV or IDE-ENV LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED=true to run.
*/
@EnabledIf("envLongRunningTestEnabled")
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class LongRunningOfferDeactivationTest extends AbstractOfferTest {
private static final int MAX_ITERATIONS = 500;
@Test
@Order(1)
public void testSellOfferAutoDisable(final TestInfo testInfo) {
PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, "US");
double mktPriceAsDouble = aliceClient.getBtcPrice("USD");
String triggerPrice = calcPriceAsString(mktPriceAsDouble, -50.0000, 4);
log.info("Current USD mkt price = {} Trigger Price = {}", mktPriceAsDouble, triggerPrice);
OfferInfo offer = aliceClient.createMarketBasedPricedOffer(SELL.name(),
"USD",
1_000_000,
1_000_000,
0.00,
defaultBuyerSecurityDepositPct.get(),
paymentAcct.getId(),
triggerPrice);
log.info("SELL offer {} created with margin based price {}.",
offer.getId(),
offer.getPrice());
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
offer = aliceClient.getOffer(offer.getId()); // Offer has trigger price now.
log.info("SELL offer should be automatically disabled when mkt price falls below {}.", offer.getTriggerPrice());
int numIterations = 0;
while (++numIterations < MAX_ITERATIONS) {
offer = aliceClient.getOffer(offer.getId());
var mktPrice = aliceClient.getBtcPrice("USD");
if (offer.getIsActivated()) {
log.info("Offer still enabled at mkt price {} > {} trigger price",
mktPrice,
offer.getTriggerPrice());
sleep(1000 * 60); // 60s
} else {
log.info("Successful test completion after offer disabled at mkt price {} < {} trigger price.",
mktPrice,
offer.getTriggerPrice());
break;
}
if (numIterations == MAX_ITERATIONS)
fail("Offer never disabled");
genBtcBlocksThenWait(1, 0);
}
}
@Test
@Order(2)
public void testBuyOfferAutoDisable(final TestInfo testInfo) {
PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, "US");
double mktPriceAsDouble = aliceClient.getBtcPrice("USD");
String triggerPrice = calcPriceAsString(mktPriceAsDouble, 50.0000, 4);
log.info("Current USD mkt price = {} Trigger Price = {}", mktPriceAsDouble, triggerPrice);
OfferInfo offer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
"USD",
1_000_000,
1_000_000,
0.00,
defaultBuyerSecurityDepositPct.get(),
paymentAcct.getId(),
triggerPrice);
log.info("BUY offer {} created with margin based price {}.",
offer.getId(),
offer.getPrice());
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
offer = aliceClient.getOffer(offer.getId()); // Offer has trigger price now.
log.info("BUY offer should be automatically disabled when mkt price rises above {}.",
offer.getTriggerPrice());
int numIterations = 0;
while (++numIterations < MAX_ITERATIONS) {
offer = aliceClient.getOffer(offer.getId());
var mktPrice = aliceClient.getBtcPrice("USD");
if (offer.getIsActivated()) {
log.info("Offer still enabled at mkt price {} < {} trigger price",
mktPrice,
offer.getTriggerPrice());
sleep(1000 * 60); // 60s
} else {
log.info("Successful test completion after offer disabled at mkt price {} > {} trigger price.",
mktPrice,
offer.getTriggerPrice());
break;
}
if (numIterations == MAX_ITERATIONS)
fail("Offer never disabled");
genBtcBlocksThenWait(1, 0);
}
}
protected static boolean envLongRunningTestEnabled() {
String envName = "LONG_RUNNING_OFFER_DEACTIVATION_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_OFFER_DEACTIVATION_TEST_ENABLED=true in bash shell."
+ "\tIf running in Intellij, set LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED=true in launcher's Environment variables field.");
return false;
}
}
}

View file

@ -71,7 +71,6 @@ public class LongRunningTradesTest extends AbstractTradeTest {
test.testTakeAlicesBuyOffer(testInfo); test.testTakeAlicesBuyOffer(testInfo);
test.testAlicesConfirmPaymentStarted(testInfo); test.testAlicesConfirmPaymentStarted(testInfo);
test.testBobsConfirmPaymentReceived(testInfo); test.testBobsConfirmPaymentReceived(testInfo);
test.testAlicesKeepFunds(testInfo);
} }
public void testTakeSellBTCOffer(final TestInfo testInfo) { public void testTakeSellBTCOffer(final TestInfo testInfo) {
@ -80,7 +79,6 @@ public class LongRunningTradesTest extends AbstractTradeTest {
test.testTakeAlicesSellOffer(testInfo); test.testTakeAlicesSellOffer(testInfo);
test.testBobsConfirmPaymentStarted(testInfo); test.testBobsConfirmPaymentStarted(testInfo);
test.testAlicesConfirmPaymentReceived(testInfo); test.testAlicesConfirmPaymentReceived(testInfo);
test.testBobsBtcWithdrawalToExternalAddress(testInfo);
} }
protected static boolean envLongRunningTestEnabled() { protected static boolean envLongRunningTestEnabled() {

View file

@ -20,6 +20,7 @@ package bisq.apitest.scenario;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -31,15 +32,21 @@ import bisq.apitest.method.offer.AbstractOfferTest;
import bisq.apitest.method.offer.CancelOfferTest; import bisq.apitest.method.offer.CancelOfferTest;
import bisq.apitest.method.offer.CreateOfferUsingFixedPriceTest; import bisq.apitest.method.offer.CreateOfferUsingFixedPriceTest;
import bisq.apitest.method.offer.CreateOfferUsingMarketPriceMarginTest; import bisq.apitest.method.offer.CreateOfferUsingMarketPriceMarginTest;
import bisq.apitest.method.offer.CreateXMROffersTest;
import bisq.apitest.method.offer.ValidateCreateOfferTest; import bisq.apitest.method.offer.ValidateCreateOfferTest;
@Slf4j @Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OfferTest extends AbstractOfferTest { public class OfferTest extends AbstractOfferTest {
@BeforeAll
public static void setUp() {
setUp(false); // Use setUp(true) for running API daemons in remote debug mode.
}
@Test @Test
@Order(1) @Order(1)
public void testAmtTooLargeShouldThrowException() { public void testCreateOfferValidation() {
ValidateCreateOfferTest test = new ValidateCreateOfferTest(); ValidateCreateOfferTest test = new ValidateCreateOfferTest();
test.testAmtTooLargeShouldThrowException(); test.testAmtTooLargeShouldThrowException();
test.testNoMatchingEURPaymentAccountShouldThrowException(); test.testNoMatchingEURPaymentAccountShouldThrowException();
@ -57,18 +64,32 @@ public class OfferTest extends AbstractOfferTest {
@Order(3) @Order(3)
public void testCreateOfferUsingFixedPrice() { public void testCreateOfferUsingFixedPrice() {
CreateOfferUsingFixedPriceTest test = new CreateOfferUsingFixedPriceTest(); CreateOfferUsingFixedPriceTest test = new CreateOfferUsingFixedPriceTest();
test.testCreateAUDXMRBuyOfferUsingFixedPrice16000(); test.testCreateAUDBTCBuyOfferUsingFixedPrice16000();
test.testCreateUSDXMRBuyOfferUsingFixedPrice100001234(); test.testCreateUSDBTCBuyOfferUsingFixedPrice100001234();
test.testCreateEURXMRSellOfferUsingFixedPrice95001234(); test.testCreateEURBTCSellOfferUsingFixedPrice95001234();
} }
@Test @Test
@Order(4) @Order(4)
public void testCreateOfferUsingMarketPriceMargin() { public void testCreateOfferUsingMarketPriceMarginPct() {
CreateOfferUsingMarketPriceMarginTest test = new CreateOfferUsingMarketPriceMarginTest(); CreateOfferUsingMarketPriceMarginTest test = new CreateOfferUsingMarketPriceMarginTest();
test.testCreateUSDXMRBuyOffer5PctPriceMargin(); test.testCreateUSDBTCBuyOffer5PctPriceMargin();
test.testCreateNZDXMRBuyOfferMinus2PctPriceMargin(); test.testCreateNZDBTCBuyOfferMinus2PctPriceMargin();
test.testCreateGBPXMRSellOfferMinus1Point5PctPriceMargin(); test.testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin();
test.testCreateBRLXMRSellOffer6Point55PctPriceMargin(); test.testCreateBRLBTCSellOffer6Point55PctPriceMargin();
test.testCreateUSDBTCBuyOfferWithTriggerPrice();
}
@Test
@Order(6)
public void testCreateXMROffers() {
CreateXMROffersTest test = new CreateXMROffersTest();
CreateXMROffersTest.createXmrPaymentAccounts();
test.testCreateFixedPriceBuy1BTCFor200KXMROffer();
test.testCreateFixedPriceSell1BTCFor200KXMROffer();
test.testCreatePriceMarginBasedBuy1BTCOfferWithTriggerPrice();
test.testCreatePriceMarginBasedSell1BTCOffer();
test.testGetAllMyXMROffers();
test.testGetAvailableXMROffers();
} }
} }

View file

@ -49,9 +49,9 @@ public class PaymentAccountTest extends AbstractPaymentAccountTest {
test.testCreateAdvancedCashAccount(testInfo); test.testCreateAdvancedCashAccount(testInfo);
test.testCreateAliPayAccount(testInfo); test.testCreateAliPayAccount(testInfo);
test.testCreateAustraliaPayidAccount(testInfo); test.testCreateAustraliaPayidAccount(testInfo);
test.testCreateCapitualAccount(testInfo);
test.testCreateCashDepositAccount(testInfo); test.testCreateCashDepositAccount(testInfo);
test.testCreateBrazilNationalBankAccount(testInfo); test.testCreateBrazilNationalBankAccount(testInfo);
test.testCreateChaseQuickPayAccount(testInfo);
test.testCreateClearXChangeAccount(testInfo); test.testCreateClearXChangeAccount(testInfo);
test.testCreateF2FAccount(testInfo); test.testCreateF2FAccount(testInfo);
test.testCreateFasterPaymentsAccount(testInfo); test.testCreateFasterPaymentsAccount(testInfo);
@ -61,6 +61,8 @@ public class PaymentAccountTest extends AbstractPaymentAccountTest {
test.testCreateMoneyBeamAccount(testInfo); test.testCreateMoneyBeamAccount(testInfo);
test.testCreateMoneyGramAccount(testInfo); test.testCreateMoneyGramAccount(testInfo);
test.testCreatePerfectMoneyAccount(testInfo); test.testCreatePerfectMoneyAccount(testInfo);
test.testCreatePaxumAccount(testInfo);
test.testCreatePayseraAccount(testInfo);
test.testCreatePopmoneyAccount(testInfo); test.testCreatePopmoneyAccount(testInfo);
test.testCreatePromptPayAccount(testInfo); test.testCreatePromptPayAccount(testInfo);
test.testCreateRevolutAccount(testInfo); test.testCreateRevolutAccount(testInfo);
@ -68,12 +70,12 @@ public class PaymentAccountTest extends AbstractPaymentAccountTest {
test.testCreateSepaInstantAccount(testInfo); test.testCreateSepaInstantAccount(testInfo);
test.testCreateSepaAccount(testInfo); test.testCreateSepaAccount(testInfo);
test.testCreateSpecificBanksAccount(testInfo); test.testCreateSpecificBanksAccount(testInfo);
test.testCreateSwiftAccount(testInfo);
test.testCreateSwishAccount(testInfo); test.testCreateSwishAccount(testInfo);
// TransferwiseAccount is only PaymentAccount with a
// tradeCurrencies field in the json form.
test.testCreateTransferwiseAccountWith1TradeCurrency(testInfo); test.testCreateTransferwiseAccountWith1TradeCurrency(testInfo);
test.testCreateTransferwiseAccountWith10TradeCurrencies(testInfo); test.testCreateTransferwiseAccountWith10TradeCurrencies(testInfo);
test.testCreateTransferwiseAccountWithSupportedTradeCurrencies(testInfo);
test.testCreateTransferwiseAccountWithInvalidBrlTradeCurrencyShouldThrowException(testInfo); test.testCreateTransferwiseAccountWithInvalidBrlTradeCurrencyShouldThrowException(testInfo);
test.testCreateTransferwiseAccountWithoutTradeCurrenciesShouldThrowException(testInfo); test.testCreateTransferwiseAccountWithoutTradeCurrenciesShouldThrowException(testInfo);

View file

@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig;
import static bisq.apitest.config.HavenoAppConfig.alicedaemon; import static bisq.apitest.config.HavenoAppConfig.alicedaemon;
import static bisq.apitest.config.HavenoAppConfig.arbdaemon; import static bisq.apitest.config.HavenoAppConfig.arbdaemon;
import static bisq.apitest.config.HavenoAppConfig.seednode; import static bisq.apitest.config.HavenoAppConfig.seednode;
@ -54,7 +55,7 @@ public class StartupTest extends MethodTest {
@BeforeAll @BeforeAll
public static void setUp() { public static void setUp() {
try { try {
callRateMeteringConfigFile = defaultRateMeterInterceptorConfig(); callRateMeteringConfigFile = getTestRateMeterInterceptorConfig();
startSupportingApps(callRateMeteringConfigFile, startSupportingApps(callRateMeteringConfigFile,
false, false,
false, false,

View file

@ -30,7 +30,10 @@ import org.junit.jupiter.api.TestMethodOrder;
import bisq.apitest.method.trade.AbstractTradeTest; import bisq.apitest.method.trade.AbstractTradeTest;
import bisq.apitest.method.trade.TakeBuyBTCOfferTest; import bisq.apitest.method.trade.TakeBuyBTCOfferTest;
import bisq.apitest.method.trade.TakeBuyBTCOfferWithNationalBankAcctTest;
import bisq.apitest.method.trade.TakeBuyXMROfferTest;
import bisq.apitest.method.trade.TakeSellBTCOfferTest; import bisq.apitest.method.trade.TakeSellBTCOfferTest;
import bisq.apitest.method.trade.TakeSellXMROfferTest;
@Slf4j @Slf4j
@ -49,7 +52,6 @@ public class TradeTest extends AbstractTradeTest {
test.testTakeAlicesBuyOffer(testInfo); test.testTakeAlicesBuyOffer(testInfo);
test.testAlicesConfirmPaymentStarted(testInfo); test.testAlicesConfirmPaymentStarted(testInfo);
test.testBobsConfirmPaymentReceived(testInfo); test.testBobsConfirmPaymentReceived(testInfo);
test.testAlicesKeepFunds(testInfo);
} }
@Test @Test
@ -59,6 +61,35 @@ public class TradeTest extends AbstractTradeTest {
test.testTakeAlicesSellOffer(testInfo); test.testTakeAlicesSellOffer(testInfo);
test.testBobsConfirmPaymentStarted(testInfo); test.testBobsConfirmPaymentStarted(testInfo);
test.testAlicesConfirmPaymentReceived(testInfo); test.testAlicesConfirmPaymentReceived(testInfo);
test.testBobsBtcWithdrawalToExternalAddress(testInfo); }
@Test
@Order(4)
public void testTakeBuyBTCOfferWithNationalBankAcct(final TestInfo testInfo) {
TakeBuyBTCOfferWithNationalBankAcctTest test = new TakeBuyBTCOfferWithNationalBankAcctTest();
test.testTakeAlicesBuyOffer(testInfo);
test.testBankAcctDetailsIncludedInContracts(testInfo);
test.testAlicesConfirmPaymentStarted(testInfo);
test.testBobsConfirmPaymentReceived(testInfo);
}
@Test
@Order(6)
public void testTakeBuyXMROffer(final TestInfo testInfo) {
TakeBuyXMROfferTest test = new TakeBuyXMROfferTest();
TakeBuyXMROfferTest.createXmrPaymentAccounts();
test.testTakeAlicesSellBTCForXMROffer(testInfo);
test.testBobsConfirmPaymentStarted(testInfo);
test.testAlicesConfirmPaymentReceived(testInfo);
}
@Test
@Order(7)
public void testTakeSellXMROffer(final TestInfo testInfo) {
TakeSellXMROfferTest test = new TakeSellXMROfferTest();
TakeBuyXMROfferTest.createXmrPaymentAccounts();
test.testTakeAlicesBuyBTCForXMROffer(testInfo);
test.testAlicesConfirmPaymentStarted(testInfo);
test.testBobsConfirmPaymentReceived(testInfo);
} }
} }

View file

@ -32,7 +32,7 @@ import lombok.extern.slf4j.Slf4j;
import static bisq.core.locale.CountryUtil.findCountryByCode; 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.CLEAR_X_CHANGE_ID;
import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById; import static bisq.core.payment.payload.PaymentMethod.getPaymentMethod;
import static java.lang.String.format; import static java.lang.String.format;
import static java.lang.System.getProperty; import static java.lang.System.getProperty;
import static java.nio.file.Files.readAllBytes; import static java.nio.file.Files.readAllBytes;
@ -74,7 +74,7 @@ public abstract class AbstractBotTest extends MethodTest {
} else { } else {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
format("This test harness bot does not work with %s payment accounts yet.", format("This test harness bot does not work with %s payment accounts yet.",
getPaymentMethodById(paymentMethodId).getDisplayString())); getPaymentMethod(paymentMethodId).getDisplayString()));
} }
} else { } else {
String countryCode = botScript.getCountryCode(); String countryCode = botScript.getCountryCode();

View file

@ -8,7 +8,7 @@ import lombok.extern.slf4j.Slf4j;
import static bisq.core.locale.CountryUtil.findCountryByCode; 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.CLEAR_X_CHANGE_ID;
import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById; import static bisq.core.payment.payload.PaymentMethod.getPaymentMethod;
import static java.lang.String.format; import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.MINUTES;
@ -62,7 +62,7 @@ class Bot {
} else { } else {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
format("This bot test does not work with %s payment accounts yet.", format("This bot test does not work with %s payment accounts yet.",
getPaymentMethodById(paymentMethodId).getDisplayString())); getPaymentMethod(paymentMethodId).getDisplayString()));
} }
} else { } else {
Country country = findCountry(botScript.getCountryCode()); Country country = findCountry(botScript.getCountryCode());

View file

@ -39,11 +39,6 @@ import bisq.cli.GrpcClient;
/** /**
* Convenience GrpcClient wrapper for bots using gRPC services. * 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"}) @SuppressWarnings({"JavaDoc", "unused"})
@Slf4j @Slf4j
@ -124,6 +119,8 @@ public class BotClient {
* @param minAmountInSatoshis * @param minAmountInSatoshis
* @param priceMarginAsPercent * @param priceMarginAsPercent
* @param securityDepositAsPercent * @param securityDepositAsPercent
* @param feeCurrency
* @param triggerPrice
* @return OfferInfo * @return OfferInfo
*/ */
public OfferInfo createOfferAtMarketBasedPrice(PaymentAccount paymentAccount, public OfferInfo createOfferAtMarketBasedPrice(PaymentAccount paymentAccount,
@ -132,14 +129,16 @@ public class BotClient {
long amountInSatoshis, long amountInSatoshis,
long minAmountInSatoshis, long minAmountInSatoshis,
double priceMarginAsPercent, double priceMarginAsPercent,
double securityDepositAsPercent) { double securityDepositAsPercent,
String triggerPrice) {
return grpcClient.createMarketBasedPricedOffer(direction, return grpcClient.createMarketBasedPricedOffer(direction,
currencyCode, currencyCode,
amountInSatoshis, amountInSatoshis,
minAmountInSatoshis, minAmountInSatoshis,
priceMarginAsPercent, priceMarginAsPercent,
securityDepositAsPercent, securityDepositAsPercent,
paymentAccount.getId()); paymentAccount.getId(),
triggerPrice);
} }
/** /**
@ -151,6 +150,7 @@ public class BotClient {
* @param minAmountInSatoshis * @param minAmountInSatoshis
* @param fixedOfferPriceAsString * @param fixedOfferPriceAsString
* @param securityDepositAsPercent * @param securityDepositAsPercent
* @param feeCurrency
* @return OfferInfo * @return OfferInfo
*/ */
public OfferInfo createOfferAtFixedPrice(PaymentAccount paymentAccount, public OfferInfo createOfferAtFixedPrice(PaymentAccount paymentAccount,
@ -225,11 +225,11 @@ public class BotClient {
} }
/** /**
* Returns true if the trade's taker deposit fee transaction is unlocked. * Returns true if the trade's taker deposit fee transaction has been confirmed.
* @param tradeId a valid trade id * @param tradeId a valid trade id
* @return boolean * @return boolean
*/ */
public boolean isTakerDepositFeeTxUnlocked(String tradeId) { public boolean isTakerDepositFeeTxConfirmed(String tradeId) {
return grpcClient.getTrade(tradeId).getIsDepositUnlocked(); return grpcClient.getTrade(tradeId).getIsDepositUnlocked();
} }
@ -278,15 +278,6 @@ public class BotClient {
grpcClient.confirmPaymentReceived(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. * Create and save a new PaymentAccount with details in the given json.
* @param json * @param json

View file

@ -33,10 +33,10 @@ import java.util.function.Supplier;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static bisq.cli.CurrencyFormat.formatMarketPrice; import static bisq.apitest.method.offer.AbstractOfferTest.defaultBuyerSecurityDepositPct;
import static bisq.cli.CurrencyFormat.formatInternalFiatPrice;
import static bisq.cli.CurrencyFormat.formatSatoshis; import static bisq.cli.CurrencyFormat.formatSatoshis;
import static bisq.common.util.MathUtils.scaleDownByPowerOf10; 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 bisq.core.payment.payload.PaymentMethod.F2F_ID;
import static java.lang.String.format; import static java.lang.String.format;
import static java.math.RoundingMode.HALF_UP; import static java.math.RoundingMode.HALF_UP;
@ -124,7 +124,8 @@ public class RandomOffer {
amount, amount,
minAmount, minAmount,
priceMargin, priceMargin,
getDefaultBuyerSecurityDepositAsPercent()); defaultBuyerSecurityDepositPct.get(),
"0" /*no trigger price*/);
} else { } else {
this.offer = botClient.createOfferAtFixedPrice(paymentAccount, this.offer = botClient.createOfferAtFixedPrice(paymentAccount,
direction, direction,
@ -132,7 +133,7 @@ public class RandomOffer {
amount, amount,
minAmount, minAmount,
fixedOfferPrice, fixedOfferPrice,
getDefaultBuyerSecurityDepositAsPercent()); defaultBuyerSecurityDepositPct.get());
} }
this.id = offer.getId(); this.id = offer.getId();
return this; return this;
@ -162,11 +163,11 @@ public class RandomOffer {
log.info(description); log.info(description);
if (useMarketBasedPrice) { if (useMarketBasedPrice) {
log.info("Offer Price Margin = {}%", priceMargin); log.info("Offer Price Margin = {}%", priceMargin);
log.info("Expected Offer Price = {} {}", formatMarketPrice(Double.parseDouble(fixedOfferPrice)), currencyCode); log.info("Expected Offer Price = {} {}", formatInternalFiatPrice(Double.parseDouble(fixedOfferPrice)), currencyCode);
} else { } else {
log.info("Fixed Offer Price = {} {}", fixedOfferPrice, currencyCode); log.info("Fixed Offer Price = {} {}", fixedOfferPrice, currencyCode);
} }
log.info("Current Market Price = {} {}", formatMarketPrice(currentMarketPrice), currencyCode); log.info("Current Market Price = {} {}", formatInternalFiatPrice(currentMarketPrice), currencyCode);
} }
} }

View file

@ -22,7 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE; import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.isShutdownCalled; import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.isShutdownCalled;
import static bisq.cli.TableFormat.formatBalancesTbls; import static bisq.cli.table.builder.TableType.BTC_BALANCE_TBL;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
@ -34,6 +34,7 @@ import bisq.apitest.scenario.bot.protocol.TakerBotProtocol;
import bisq.apitest.scenario.bot.script.BashScriptGenerator; import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.script.BotScript; import bisq.apitest.scenario.bot.script.BotScript;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException; import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import bisq.cli.table.builder.TableBuilder;
@Slf4j @Slf4j
public public
@ -74,10 +75,14 @@ class RobotBob extends Bot {
throw new IllegalStateException(botProtocol.getClass().getSimpleName() + " failed to complete."); throw new IllegalStateException(botProtocol.getClass().getSimpleName() + " failed to complete.");
} }
StringBuilder balancesBuilder = new StringBuilder();
balancesBuilder.append("BTC").append("\n");
balancesBuilder.append(new TableBuilder(BTC_BALANCE_TBL, botClient.getBalance().getBtc()).build().toString()).append("\n");
log.info("Completed {} successful trade{}. Current Balance:\n{}", log.info("Completed {} successful trade{}. Current Balance:\n{}",
++numTrades, ++numTrades,
numTrades == 1 ? "" : "s", numTrades == 1 ? "" : "s",
formatBalancesTbls(botClient.getBalance())); balancesBuilder);
if (numTrades < actions.length) { if (numTrades < actions.length) {
try { try {

View file

@ -39,6 +39,7 @@ import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.*; import static bisq.apitest.scenario.bot.protocol.ProtocolStep.*;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled; import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL;
import static java.lang.String.format; import static java.lang.String.format;
import static java.lang.System.currentTimeMillis; import static java.lang.System.currentTimeMillis;
import static java.util.Arrays.stream; import static java.util.Arrays.stream;
@ -50,7 +51,7 @@ import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.BotClient; import bisq.apitest.scenario.bot.BotClient;
import bisq.apitest.scenario.bot.script.BashScriptGenerator; import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException; import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import bisq.cli.TradeFormat; import bisq.cli.table.builder.TableBuilder;
@Slf4j @Slf4j
public abstract class BotProtocol { public abstract class BotProtocol {
@ -110,7 +111,7 @@ public abstract class BotProtocol {
log.info("Starting protocol step {}. Bot will shutdown if step not completed within {} minutes.", log.info("Starting protocol step {}. Bot will shutdown if step not completed within {} minutes.",
currentProtocolStep.name(), MILLISECONDS.toMinutes(protocolStepTimeLimitInMs)); currentProtocolStep.name(), MILLISECONDS.toMinutes(protocolStepTimeLimitInMs));
if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_UNLOCKED)) { if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED)) {
log.info("Generate a btc block to trigger taker's deposit fee tx confirmation."); log.info("Generate a btc block to trigger taker's deposit fee tx confirmation.");
createGenerateBtcBlockScript(); createGenerateBtcBlockScript();
} }
@ -133,7 +134,8 @@ public abstract class BotProtocol {
try { try {
var t = this.getBotClient().getTrade(trade.getTradeId()); var t = this.getBotClient().getTrade(trade.getTradeId());
if (t.getIsPaymentSent()) { if (t.getIsPaymentSent()) {
log.info("Buyer has started payment for trade:\n{}", TradeFormat.format(t)); log.info("Buyer has started payment for trade:\n{}",
new TableBuilder(TRADE_DETAIL_TBL, t).build().toString());
return t; return t;
} }
} catch (Exception ex) { } catch (Exception ex) {
@ -167,7 +169,8 @@ public abstract class BotProtocol {
try { try {
var t = this.getBotClient().getTrade(trade.getTradeId()); var t = this.getBotClient().getTrade(trade.getTradeId());
if (t.getIsPaymentReceived()) { if (t.getIsPaymentReceived()) {
log.info("Seller has received payment for trade:\n{}", TradeFormat.format(t)); log.info("Seller has received payment for trade:\n{}",
new TableBuilder(TRADE_DETAIL_TBL, t).build().toString());
return t; return t;
} }
} catch (Exception ex) { } catch (Exception ex) {
@ -202,7 +205,7 @@ public abstract class BotProtocol {
if (t.getIsPayoutPublished()) { if (t.getIsPayoutPublished()) {
log.info("Payout tx {} has been published for trade:\n{}", log.info("Payout tx {} has been published for trade:\n{}",
t.getPayoutTxId(), t.getPayoutTxId(),
TradeFormat.format(t)); new TableBuilder(TRADE_DETAIL_TBL, t).build().toString());
return t; return t;
} }
} catch (Exception ex) { } catch (Exception ex) {
@ -219,21 +222,6 @@ public abstract class BotProtocol {
} }
}; };
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) { protected void createPaymentStartedScript(TradeInfo trade) {
File script = bashScriptGenerator.createPaymentStartedScript(trade); File script = bashScriptGenerator.createPaymentStartedScript(trade);
printCliHintAndOrScript(script, "The manual CLI side can send a 'payment started' message"); printCliHintAndOrScript(script, "The manual CLI side can send a 'payment started' message");
@ -281,12 +269,12 @@ public abstract class BotProtocol {
} }
private void waitForTakerFeeTxConfirmed(String tradeId) { private void waitForTakerFeeTxConfirmed(String tradeId) {
waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_UNLOCKED); waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED);
} }
private void waitForTakerDepositFee(String tradeId, ProtocolStep depositTxProtocolStep) { private void waitForTakerDepositFee(String tradeId, ProtocolStep depositTxProtocolStep) {
initProtocolStep.accept(depositTxProtocolStep); initProtocolStep.accept(depositTxProtocolStep);
validateCurrentProtocolStep(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED, WAIT_FOR_TAKER_DEPOSIT_TX_UNLOCKED); validateCurrentProtocolStep(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED);
try { try {
log.info(waitingForDepositFeeTxMsg(tradeId)); log.info(waitingForDepositFeeTxMsg(tradeId));
while (isWithinProtocolStepTimeLimit()) { while (isWithinProtocolStepTimeLimit()) {
@ -316,8 +304,8 @@ public abstract class BotProtocol {
if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) && trade.getIsDepositPublished()) { if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) && trade.getIsDepositPublished()) {
log.info("Taker deposit fee tx {} has been published.", trade.getTakerDepositTxId()); log.info("Taker deposit fee tx {} has been published.", trade.getTakerDepositTxId());
return true; return true;
} else if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_UNLOCKED) && trade.getIsDepositUnlocked()) { } else if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED) && trade.getIsDepositUnlocked()) {
log.info("Taker deposit fee tx {} is unlocked.", trade.getTakerDepositTxId()); log.info("Taker deposit fee tx {} has been confirmed.", trade.getTakerDepositTxId());
return true; return true;
} else { } else {
return false; return false;

View file

@ -16,8 +16,8 @@ import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE; 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.protocol.ProtocolStep.WAIT_FOR_OFFER_TAKER;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled; import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
import static bisq.cli.TableFormat.formatOfferTable; import static bisq.cli.table.builder.TableType.OFFER_TBL;
import static java.util.Collections.singletonList; import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL;
@ -26,7 +26,7 @@ import bisq.apitest.scenario.bot.BotClient;
import bisq.apitest.scenario.bot.RandomOffer; import bisq.apitest.scenario.bot.RandomOffer;
import bisq.apitest.scenario.bot.script.BashScriptGenerator; import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException; import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import bisq.cli.TradeFormat; import bisq.cli.table.builder.TableBuilder;
@Slf4j @Slf4j
public class MakerBotProtocol extends BotProtocol { public class MakerBotProtocol extends BotProtocol {
@ -56,16 +56,13 @@ public class MakerBotProtocol extends BotProtocol {
: waitForPaymentStartedMessage.andThen(sendPaymentReceivedMessage); : waitForPaymentStartedMessage.andThen(sendPaymentReceivedMessage);
completeFiatTransaction.apply(trade); completeFiatTransaction.apply(trade);
Function<TradeInfo, TradeInfo> closeTrade = waitForPayoutTx.andThen(keepFundsFromTrade);
closeTrade.apply(trade);
currentProtocolStep = DONE; currentProtocolStep = DONE;
} }
private final Supplier<OfferInfo> randomOffer = () -> { private final Supplier<OfferInfo> randomOffer = () -> {
checkIfShutdownCalled("Interrupted before creating random offer."); checkIfShutdownCalled("Interrupted before creating random offer.");
OfferInfo offer = new RandomOffer(botClient, paymentAccount).create().getOffer(); OfferInfo offer = new RandomOffer(botClient, paymentAccount).create().getOffer();
log.info("Created random {} offer\n{}", currencyCode, formatOfferTable(singletonList(offer), currencyCode)); log.info("Created random {} offer\n{}", currencyCode, new TableBuilder(OFFER_TBL, offer).build());
return offer; return offer;
}; };
@ -98,7 +95,9 @@ public class MakerBotProtocol extends BotProtocol {
private Optional<TradeInfo> getNewTrade(String offerId) { private Optional<TradeInfo> getNewTrade(String offerId) {
try { try {
var trade = botClient.getTrade(offerId); var trade = botClient.getTrade(offerId);
log.info("Offer {} was taken, new trade:\n{}", offerId, TradeFormat.format(trade)); log.info("Offer {} was taken, new trade:\n{}",
offerId,
new TableBuilder(TRADE_DETAIL_TBL, trade).build().toString());
return Optional.of(trade); return Optional.of(trade);
} catch (Exception ex) { } catch (Exception ex) {
// Get trade will throw a non-fatal gRPC exception if not found. // Get trade will throw a non-fatal gRPC exception if not found.

View file

@ -6,12 +6,12 @@ public enum ProtocolStep {
TAKE_OFFER, TAKE_OFFER,
WAIT_FOR_OFFER_TAKER, WAIT_FOR_OFFER_TAKER,
WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED, WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED,
WAIT_FOR_TAKER_DEPOSIT_TX_UNLOCKED, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED,
SEND_PAYMENT_STARTED_MESSAGE, SEND_PAYMENT_STARTED_MESSAGE,
WAIT_FOR_PAYMENT_STARTED_MESSAGE, WAIT_FOR_PAYMENT_STARTED_MESSAGE,
SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE, SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE,
WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE, WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE,
WAIT_FOR_PAYOUT_TX, WAIT_FOR_PAYOUT_TX,
KEEP_FUNDS, CLOSE_TRADE,
DONE DONE
} }

View file

@ -17,7 +17,7 @@ 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.FIND_OFFER;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.TAKE_OFFER; import static bisq.apitest.scenario.bot.protocol.ProtocolStep.TAKE_OFFER;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled; import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
import static bisq.cli.TableFormat.formatOfferTable; import static bisq.cli.table.builder.TableType.OFFER_TBL;
import static bisq.core.payment.payload.PaymentMethod.F2F_ID; import static bisq.core.payment.payload.PaymentMethod.F2F_ID;
@ -26,6 +26,7 @@ import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.BotClient; import bisq.apitest.scenario.bot.BotClient;
import bisq.apitest.scenario.bot.script.BashScriptGenerator; import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException; import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import bisq.cli.table.builder.TableBuilder;
@Slf4j @Slf4j
public class TakerBotProtocol extends BotProtocol { public class TakerBotProtocol extends BotProtocol {
@ -55,16 +56,13 @@ public class TakerBotProtocol extends BotProtocol {
: sendPaymentStartedMessage.andThen(waitForPaymentReceivedConfirmation); : sendPaymentStartedMessage.andThen(waitForPaymentReceivedConfirmation);
completeFiatTransaction.apply(trade); completeFiatTransaction.apply(trade);
Function<TradeInfo, TradeInfo> closeTrade = waitForPayoutTx.andThen(keepFundsFromTrade);
closeTrade.apply(trade);
currentProtocolStep = DONE; currentProtocolStep = DONE;
} }
private final Supplier<Optional<OfferInfo>> firstOffer = () -> { private final Supplier<Optional<OfferInfo>> firstOffer = () -> {
var offers = botClient.getOffers(currencyCode); var offers = botClient.getOffers(currencyCode);
if (offers.size() > 0) { if (offers.size() > 0) {
log.info("Offers found:\n{}", formatOfferTable(offers, currencyCode)); log.info("Offers found:\n{}", new TableBuilder(OFFER_TBL, offers).build());
OfferInfo offer = offers.get(0); OfferInfo offer = offers.get(0);
log.info("Will take first offer {}", offer.getId()); log.info("Will take first offer {}", offer.getId());
return Optional.of(offer); return Optional.of(offer);
@ -107,7 +105,6 @@ public class TakerBotProtocol extends BotProtocol {
private void createMakeOfferScript() { private void createMakeOfferScript() {
String direction = RANDOM.nextBoolean() ? "BUY" : "SELL"; String direction = RANDOM.nextBoolean() ? "BUY" : "SELL";
String feeCurrency = "BTC";
boolean createMarginPricedOffer = RANDOM.nextBoolean(); boolean createMarginPricedOffer = RANDOM.nextBoolean();
// If not using an F2F account, don't go over possible 0.01 BTC // If not using an F2F account, don't go over possible 0.01 BTC
// limit if account is not signed. // limit if account is not signed.
@ -120,15 +117,13 @@ public class TakerBotProtocol extends BotProtocol {
currencyCode, currencyCode,
amount, amount,
"0.0", "0.0",
"15.0", "15.0");
feeCurrency);
} else { } else {
script = bashScriptGenerator.createMakeFixedPricedOfferScript(direction, script = bashScriptGenerator.createMakeFixedPricedOfferScript(direction,
currencyCode, currencyCode,
amount, amount,
botClient.getCurrentBTCMarketPriceAsIntegerString(currencyCode), botClient.getCurrentBTCMarketPriceAsIntegerString(currencyCode),
"15.0", "15.0");
feeCurrency);
} }
printCliHintAndOrScript(script, "The manual CLI side can create an offer"); printCliHintAndOrScript(script, "The manual CLI side can create an offer");
} }

View file

@ -67,8 +67,7 @@ public class BashScriptGenerator {
String currencyCode, String currencyCode,
String amount, String amount,
String marketPriceMargin, String marketPriceMargin,
String securityDeposit, String securityDeposit) {
String feeCurrency) {
String makeOfferCmd = format("%s createoffer --payment-account=%s " String makeOfferCmd = format("%s createoffer --payment-account=%s "
+ " --direction=%s" + " --direction=%s"
+ " --currency-code=%s" + " --currency-code=%s"
@ -82,8 +81,7 @@ public class BashScriptGenerator {
currencyCode, currencyCode,
amount, amount,
marketPriceMargin, marketPriceMargin,
securityDeposit, securityDeposit);
feeCurrency);
String getOffersCmd = format("%s getmyoffers --direction=%s --currency-code=%s", String getOffersCmd = format("%s getmyoffers --direction=%s --currency-code=%s",
cliBase, cliBase,
direction, direction,
@ -98,8 +96,7 @@ public class BashScriptGenerator {
String currencyCode, String currencyCode,
String amount, String amount,
String fixedPrice, String fixedPrice,
String securityDeposit, String securityDeposit) {
String feeCurrency) {
String makeOfferCmd = format("%s createoffer --payment-account=%s " String makeOfferCmd = format("%s createoffer --payment-account=%s "
+ " --direction=%s" + " --direction=%s"
+ " --currency-code=%s" + " --currency-code=%s"
@ -113,8 +110,7 @@ public class BashScriptGenerator {
currencyCode, currencyCode,
amount, amount,
fixedPrice, fixedPrice,
securityDeposit, securityDeposit);
feeCurrency);
String getOffersCmd = format("%s getmyoffers --direction=%s --currency-code=%s", String getOffersCmd = format("%s getmyoffers --direction=%s --currency-code=%s",
cliBase, cliBase,
direction, direction,
@ -167,10 +163,10 @@ public class BashScriptGenerator {
} }
public File createKeepFundsScript(TradeInfo trade) { public File createKeepFundsScript(TradeInfo trade) {
String paymentStartedCmd = format("%s keepfunds --trade-id=%s", cliBase, trade.getTradeId()); String paymentStartedCmd = format("%s closetrade --trade-id=%s", cliBase, trade.getTradeId());
String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId()); String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId());
String getBalanceCmd = format("%s getbalance", cliBase); String getBalanceCmd = format("%s getbalance", cliBase);
return createCliScript("keepfunds.sh", return createCliScript("closetrade.sh",
paymentStartedCmd, paymentStartedCmd,
"sleep 2", "sleep 2",
getTradeCmd, getTradeCmd,

View file

@ -17,8 +17,9 @@
package bisq.apitest.scenario.bot.script; package bisq.apitest.scenario.bot.script;
import bisq.core.util.JsonUtil;
import bisq.common.file.JsonFileManager; import bisq.common.file.JsonFileManager;
import bisq.common.util.Utilities;
import joptsimple.BuiltinHelpFormatter; import joptsimple.BuiltinHelpFormatter;
import joptsimple.OptionParser; import joptsimple.OptionParser;
@ -214,7 +215,7 @@ public class BotScriptGenerator {
} }
private String generateBotScriptTemplate() { private String generateBotScriptTemplate() {
return Utilities.objectToJson(new BotScript( return JsonUtil.objectToJson(new BotScript(
useTestHarness, useTestHarness,
botPaymentMethodId, botPaymentMethodId,
countryCode, countryCode,

View file

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

View file

@ -31,6 +31,7 @@ configure(subprojects) {
bcVersion = '1.63' bcVersion = '1.63'
bitcoinjVersion = '2a80db4' bitcoinjVersion = '2a80db4'
codecVersion = '1.13' codecVersion = '1.13'
cowwocVersion = '1.2'
easybindVersion = '1.0.3' easybindVersion = '1.0.3'
easyVersion = '4.0.1' easyVersion = '4.0.1'
findbugsVersion = '3.0.2' findbugsVersion = '3.0.2'
@ -404,6 +405,7 @@ configure(project(':cli')) {
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion")
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
testCompileOnly "org.projectlombok:lombok:$lombokVersion" testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testImplementation "org.bitbucket.cowwoc:diff-match-patch:$cowwocVersion"
testRuntime "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" testRuntime "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
} }

71
cli/package/create-cli-dist.sh Executable file
View file

@ -0,0 +1,71 @@
#! /bin/bash
VERSION="$1"
if [[ -z "$VERSION" ]]; then
VERSION="SNAPSHOT"
fi
export BISQ_RELEASE_NAME="bisq-cli-$VERSION"
export BISQ_RELEASE_ZIP_NAME="$BISQ_RELEASE_NAME.zip"
export GRADLE_DIST_NAME="cli.tar"
export GRADLE_DIST_PATH="../build/distributions/$GRADLE_DIST_NAME"
arrangegradledist() {
# Arrange $BISQ_RELEASE_NAME directory structure to contain a runnable
# jar at the top-level, and a lib dir containing dependencies:
# .
# |
# |__ cli.jar
# |__ lib
# |__ |__ dep1.jar
# |__ |__ dep2.jar
# |__ |__ ...
# Copy the build's distribution tarball to this directory.
cp -v $GRADLE_DIST_PATH .
# Create a clean directory to hold the tarball's content.
rm -rf $BISQ_RELEASE_NAME
mkdir $BISQ_RELEASE_NAME
# Extract the tarball's content into $BISQ_RELEASE_NAME.
tar -xf $GRADLE_DIST_NAME -C $BISQ_RELEASE_NAME
cd $BISQ_RELEASE_NAME
# Rearrange $BISQ_RELEASE_NAME contents: move the lib directory up one level.
mv -v cli/lib .
# Rearrange $BISQ_RELEASE_NAME contents: remove the cli/bin and cli directories.
rm -rf cli
# Rearrange $BISQ_RELEASE_NAME contents: move the lib/cli.jar up one level.
mv -v lib/cli.jar .
}
writemanifest() {
# Make the cli.jar runnable, and define its dependencies in a MANIFEST.MF update.
echo "Main-Class: bisq.cli.CliMain" > manifest-update.txt
printf "Class-Path: " >> manifest-update.txt
for file in lib/*
do
# Each new line in the classpath must be preceded by two spaces.
printf " %s\n" "$file" >> manifest-update.txt
done
}
updatemanifest() {
# Append contents of to cli.jar's MANIFEST.MF.
jar uvfm cli.jar manifest-update.txt
}
ziprelease() {
cd ..
zip -r $BISQ_RELEASE_ZIP_NAME $BISQ_RELEASE_NAME/lib $BISQ_RELEASE_NAME/cli.jar
}
cleanup() {
rm -v ./$GRADLE_DIST_NAME
rm -r ./$BISQ_RELEASE_NAME
}
arrangegradledist
writemanifest
updatemanifest
ziprelease
cleanup

View file

@ -18,6 +18,7 @@
package bisq.cli; package bisq.cli;
import bisq.proto.grpc.OfferInfo; import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.TradeInfo;
import io.grpc.StatusRuntimeException; import io.grpc.StatusRuntimeException;
@ -39,18 +40,18 @@ import java.util.List;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static bisq.cli.CurrencyFormat.formatMarketPrice; import static bisq.cli.CurrencyFormat.formatInternalFiatPrice;
import static bisq.cli.CurrencyFormat.formatTxFeeRateInfo; import static bisq.cli.CurrencyFormat.formatTxFeeRateInfo;
import static bisq.cli.CurrencyFormat.toSatoshis; import static bisq.cli.CurrencyFormat.toSatoshis;
import static bisq.cli.CurrencyFormat.toSecurityDepositAsPct;
import static bisq.cli.Method.*; import static bisq.cli.Method.*;
import static bisq.cli.TableFormat.*;
import static bisq.cli.opts.OptLabel.*; import static bisq.cli.opts.OptLabel.*;
import static bisq.cli.table.builder.TableType.*;
import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED;
import static bisq.proto.grpc.GetTradesRequest.Category.OPEN;
import static java.lang.String.format; import static java.lang.String.format;
import static java.lang.System.err; import static java.lang.System.err;
import static java.lang.System.exit; import static java.lang.System.exit;
import static java.lang.System.out; import static java.lang.System.out;
import static java.util.Collections.singletonList;
@ -62,11 +63,12 @@ import bisq.cli.opts.CreatePaymentAcctOptionParser;
import bisq.cli.opts.GetAddressBalanceOptionParser; import bisq.cli.opts.GetAddressBalanceOptionParser;
import bisq.cli.opts.GetBTCMarketPriceOptionParser; import bisq.cli.opts.GetBTCMarketPriceOptionParser;
import bisq.cli.opts.GetBalanceOptionParser; import bisq.cli.opts.GetBalanceOptionParser;
import bisq.cli.opts.GetOfferOptionParser;
import bisq.cli.opts.GetOffersOptionParser; import bisq.cli.opts.GetOffersOptionParser;
import bisq.cli.opts.GetPaymentAcctFormOptionParser; import bisq.cli.opts.GetPaymentAcctFormOptionParser;
import bisq.cli.opts.GetTradeOptionParser; import bisq.cli.opts.GetTradeOptionParser;
import bisq.cli.opts.GetTradesOptionParser;
import bisq.cli.opts.GetTransactionOptionParser; import bisq.cli.opts.GetTransactionOptionParser;
import bisq.cli.opts.OfferIdOptionParser;
import bisq.cli.opts.RegisterDisputeAgentOptionParser; import bisq.cli.opts.RegisterDisputeAgentOptionParser;
import bisq.cli.opts.RemoveWalletPasswordOptionParser; import bisq.cli.opts.RemoveWalletPasswordOptionParser;
import bisq.cli.opts.SendBtcOptionParser; import bisq.cli.opts.SendBtcOptionParser;
@ -76,6 +78,7 @@ import bisq.cli.opts.SimpleMethodOptionParser;
import bisq.cli.opts.TakeOfferOptionParser; import bisq.cli.opts.TakeOfferOptionParser;
import bisq.cli.opts.UnlockWalletOptionParser; import bisq.cli.opts.UnlockWalletOptionParser;
import bisq.cli.opts.WithdrawFundsOptionParser; import bisq.cli.opts.WithdrawFundsOptionParser;
import bisq.cli.table.builder.TableBuilder;
/** /**
* A command-line client for the Bisq gRPC API. * A command-line client for the Bisq gRPC API.
@ -167,15 +170,14 @@ public class CliMain {
var balances = client.getBalances(currencyCode); var balances = client.getBalances(currencyCode);
switch (currencyCode.toUpperCase()) { switch (currencyCode.toUpperCase()) {
case "BTC": case "BTC":
out.println(formatBtcBalanceInfoTbl(balances.getBtc())); new TableBuilder(BTC_BALANCE_TBL, balances.getBtc()).build().print(out);
break; break;
case "XMR":
out.println(formatXmrBalanceInfoTbl(balances.getXmr()));
break;
case "": case "":
default: default: {
out.println(formatBalancesTbls(balances)); out.println("BTC");
new TableBuilder(BTC_BALANCE_TBL, balances.getBtc()).build().print(out);
break; break;
}
} }
return; return;
} }
@ -187,7 +189,7 @@ public class CliMain {
} }
var address = opts.getAddress(); var address = opts.getAddress();
var addressBalance = client.getAddressBalance(address); var addressBalance = client.getAddressBalance(address);
out.println(formatAddressBalanceTbl(singletonList(addressBalance))); new TableBuilder(ADDRESS_BALANCE_TBL, addressBalance).build().print(out);
return; return;
} }
case getbtcprice: { case getbtcprice: {
@ -198,7 +200,7 @@ public class CliMain {
} }
var currencyCode = opts.getCurrencyCode(); var currencyCode = opts.getCurrencyCode();
var price = client.getBtcPrice(currencyCode); var price = client.getBtcPrice(currencyCode);
out.println(formatMarketPrice(price)); out.println(formatInternalFiatPrice(price));
return; return;
} }
case getfundingaddresses: { case getfundingaddresses: {
@ -207,7 +209,7 @@ public class CliMain {
return; return;
} }
var fundingAddresses = client.getFundingAddresses(); var fundingAddresses = client.getFundingAddresses();
out.println(formatAddressBalanceTbl(fundingAddresses)); new TableBuilder(ADDRESS_BALANCE_TBL, fundingAddresses).build().print(out);
return; return;
} }
case sendbtc: { case sendbtc: {
@ -269,7 +271,7 @@ public class CliMain {
} }
var txId = opts.getTxId(); var txId = opts.getTxId();
var tx = client.getTransaction(txId); var tx = client.getTransaction(txId);
out.println(TransactionFormat.format(tx)); new TableBuilder(TRANSACTION_TBL, tx).build().print(out);
return; return;
} }
case createoffer: { case createoffer: {
@ -285,18 +287,21 @@ public class CliMain {
var minAmount = toSatoshis(opts.getMinAmount()); var minAmount = toSatoshis(opts.getMinAmount());
var useMarketBasedPrice = opts.isUsingMktPriceMargin(); var useMarketBasedPrice = opts.isUsingMktPriceMargin();
var fixedPrice = opts.getFixedPrice(); var fixedPrice = opts.getFixedPrice();
var marketPriceMargin = opts.getMktPriceMarginAsBigDecimal(); var marketPriceMarginPct = opts.getMktPriceMarginPct();
var securityDeposit = toSecurityDepositAsPct(opts.getSecurityDeposit()); var securityDepositPct = opts.getSecurityDepositPct();
var offer = client.createOffer(direction, var triggerPrice = "0"; // Cannot be defined until the new offer is added to book.
OfferInfo offer;
offer = client.createOffer(direction,
currencyCode, currencyCode,
amount, amount,
minAmount, minAmount,
useMarketBasedPrice, useMarketBasedPrice,
fixedPrice, fixedPrice,
marketPriceMargin.doubleValue(), marketPriceMarginPct,
securityDeposit, securityDepositPct,
paymentAcctId); paymentAcctId,
out.println(formatOfferTable(singletonList(offer), currencyCode)); triggerPrice);
new TableBuilder(OFFER_TBL, offer).build().print(out);
return; return;
} }
case canceloffer: { case canceloffer: {
@ -311,25 +316,25 @@ public class CliMain {
return; return;
} }
case getoffer: { case getoffer: {
var opts = new GetOfferOptionParser(args).parse(); var opts = new OfferIdOptionParser(args).parse();
if (opts.isForHelp()) { if (opts.isForHelp()) {
out.println(client.getMethodHelp(method)); out.println(client.getMethodHelp(method));
return; return;
} }
var offerId = opts.getOfferId(); var offerId = opts.getOfferId();
var offer = client.getOffer(offerId); var offer = client.getOffer(offerId);
out.println(formatOfferTable(singletonList(offer), offer.getCounterCurrencyCode())); new TableBuilder(OFFER_TBL, offer).build().print(out);
return; return;
} }
case getmyoffer: { case getmyoffer: {
var opts = new GetOfferOptionParser(args).parse(); var opts = new OfferIdOptionParser(args).parse();
if (opts.isForHelp()) { if (opts.isForHelp()) {
out.println(client.getMethodHelp(method)); out.println(client.getMethodHelp(method));
return; return;
} }
var offerId = opts.getOfferId(); var offerId = opts.getOfferId();
var offer = client.getMyOffer(offerId); var offer = client.getMyOffer(offerId);
out.println(formatOfferTable(singletonList(offer), offer.getCounterCurrencyCode())); new TableBuilder(OFFER_TBL, offer).build().print(out);
return; return;
} }
case getoffers: { case getoffers: {
@ -344,7 +349,7 @@ public class CliMain {
if (offers.isEmpty()) if (offers.isEmpty())
out.printf("no %s %s offers found%n", direction, currencyCode); out.printf("no %s %s offers found%n", direction, currencyCode);
else else
out.println(formatOfferTable(offers, currencyCode)); new TableBuilder(OFFER_TBL, offers).build().print(out);
return; return;
} }
@ -360,11 +365,12 @@ public class CliMain {
if (offers.isEmpty()) if (offers.isEmpty())
out.printf("no %s %s offers found%n", direction, currencyCode); out.printf("no %s %s offers found%n", direction, currencyCode);
else else
out.println(formatOfferTable(offers, currencyCode)); new TableBuilder(OFFER_TBL, offers).build().print(out);
return; return;
} }
case takeoffer: { case takeoffer: {
var opts = new TakeOfferOptionParser(args).parse(); var opts = new TakeOfferOptionParser(args).parse();
if (opts.isForHelp()) { if (opts.isForHelp()) {
out.println(client.getMethodHelp(method)); out.println(client.getMethodHelp(method));
@ -389,10 +395,30 @@ public class CliMain {
if (showContract) if (showContract)
out.println(trade.getContractAsJson()); out.println(trade.getContractAsJson());
else else
out.println(TradeFormat.format(trade)); new TableBuilder(TRADE_DETAIL_TBL, trade).build().print(out);
return; return;
} }
case gettrades: {
var opts = new GetTradesOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var category = opts.getCategory();
var trades = category.equals(OPEN)
? client.getOpenTrades()
: client.getTradeHistory(category);
if (trades.isEmpty()) {
out.printf("no %s trades found%n", category.name().toLowerCase());
} else {
var tableType = category.equals(OPEN)
? OPEN_TRADES_TBL
: category.equals(CLOSED) ? CLOSED_TRADES_TBL : FAILED_TRADES_TBL;
new TableBuilder(tableType, trades).build().print(out);
}
return;
}
case confirmpaymentstarted: { case confirmpaymentstarted: {
var opts = new GetTradeOptionParser(args).parse(); var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) { if (opts.isForHelp()) {
@ -415,17 +441,6 @@ public class CliMain {
out.printf("trade %s payment received message sent%n", tradeId); out.printf("trade %s payment received message sent%n", tradeId);
return; return;
} }
case keepfunds: {
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
client.keepFunds(tradeId);
out.printf("funds from trade %s saved in bisq wallet%n", tradeId);
return;
}
case withdrawfunds: { case withdrawfunds: {
var opts = new WithdrawFundsOptionParser(args).parse(); var opts = new WithdrawFundsOptionParser(args).parse();
if (opts.isForHelp()) { if (opts.isForHelp()) {
@ -434,7 +449,7 @@ public class CliMain {
} }
var tradeId = opts.getTradeId(); var tradeId = opts.getTradeId();
var address = opts.getAddress(); var address = opts.getAddress();
// Multi-word memos must be double quoted. // Multi-word memos must be double-quoted.
var memo = opts.getMemo(); var memo = opts.getMemo();
client.withdrawFunds(tradeId, address, memo); client.withdrawFunds(tradeId, address, memo);
out.printf("trade %s funds sent to btc address %s%n", tradeId, address); out.printf("trade %s funds sent to btc address %s%n", tradeId, address);
@ -481,11 +496,12 @@ public class CliMain {
} }
var paymentAccount = client.createPaymentAccount(jsonString); var paymentAccount = client.createPaymentAccount(jsonString);
out.println("payment account saved"); out.println("payment account saved");
out.println(formatPaymentAcctTbl(singletonList(paymentAccount))); new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount).build().print(out);
return; return;
} }
case createcryptopaymentacct: { case createcryptopaymentacct: {
var opts = new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse(); var opts =
new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse();
if (opts.isForHelp()) { if (opts.isForHelp()) {
out.println(client.getMethodHelp(method)); out.println(client.getMethodHelp(method));
return; return;
@ -499,7 +515,7 @@ public class CliMain {
address, address,
isTradeInstant); isTradeInstant);
out.println("payment account saved"); out.println("payment account saved");
out.println(formatPaymentAcctTbl(singletonList(paymentAccount))); new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount).build().print(out);
return; return;
} }
case getpaymentaccts: { case getpaymentaccts: {
@ -509,7 +525,7 @@ public class CliMain {
} }
var paymentAccounts = client.getPaymentAccounts(); var paymentAccounts = client.getPaymentAccounts();
if (paymentAccounts.size() > 0) if (paymentAccounts.size() > 0)
out.println(formatPaymentAcctTbl(paymentAccounts)); new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccounts).build().print(out);
else else
out.println("no payment accounts are saved"); out.println("no payment accounts are saved");
@ -585,7 +601,8 @@ public class CliMain {
} }
} }
} catch (StatusRuntimeException ex) { } catch (StatusRuntimeException ex) {
// Remove the leading gRPC status code (e.g. "UNKNOWN: ") from the message // Remove the leading gRPC status code, e.g., INVALID_ARGUMENT,
// NOT_FOUND, ..., UNKNOWN from the exception message.
String message = ex.getMessage().replaceFirst("^[A-Z_]+: ", ""); String message = ex.getMessage().replaceFirst("^[A-Z_]+: ", "");
if (message.equals("io exception")) if (message.equals("io exception"))
throw new RuntimeException(message + ", server may not be running", ex); throw new RuntimeException(message + ", server may not be running", ex);
@ -666,7 +683,7 @@ public class CliMain {
stream.format(rowFormat, "------", "------", "------------"); stream.format(rowFormat, "------", "------", "------------");
stream.format(rowFormat, getversion.name(), "", "Get server version"); stream.format(rowFormat, getversion.name(), "", "Get server version");
stream.println(); stream.println();
stream.format(rowFormat, getbalance.name(), "[--currency-code=<btc>]", "Get server wallet balances"); stream.format(rowFormat, getbalance.name(), "[--currency-code=<bsq|btc>]", "Get server wallet balances");
stream.println(); stream.println();
stream.format(rowFormat, getaddressbalance.name(), "--address=<btc-address>", "Get server wallet address balance"); stream.format(rowFormat, getaddressbalance.name(), "--address=<btc-address>", "Get server wallet address balance");
stream.println(); stream.println();
@ -674,11 +691,14 @@ public class CliMain {
stream.println(); stream.println();
stream.format(rowFormat, getfundingaddresses.name(), "", "Get BTC funding addresses"); stream.format(rowFormat, getfundingaddresses.name(), "", "Get BTC funding addresses");
stream.println(); stream.println();
stream.format(rowFormat, getunusedbsqaddress.name(), "", "Get unused BSQ address");
stream.println();
stream.format(rowFormat, "", "[--tx-fee-rate=<sats/byte>]", "");
stream.println();
stream.format(rowFormat, sendbtc.name(), "--address=<btc-address> --amount=<btc-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, "", "[--tx-fee-rate=<sats/byte>]", "");
stream.format(rowFormat, "", "[--memo=<\"memo\">]", ""); stream.format(rowFormat, "", "[--memo=<\"memo\">]", "");
stream.println(); stream.println();
stream.println();
stream.format(rowFormat, gettxfeerate.name(), "", "Get current tx fee rate in sats/byte"); stream.format(rowFormat, gettxfeerate.name(), "", "Get current tx fee rate in sats/byte");
stream.println(); stream.println();
stream.format(rowFormat, settxfeerate.name(), "--tx-fee-rate=<sats/byte>", "Set custom tx fee rate in sats/byte"); stream.format(rowFormat, settxfeerate.name(), "--tx-fee-rate=<sats/byte>", "Set custom tx fee rate in sats/byte");
@ -692,9 +712,17 @@ public class CliMain {
stream.format(rowFormat, "", "--currency-code=<currency-code> \\", ""); stream.format(rowFormat, "", "--currency-code=<currency-code> \\", "");
stream.format(rowFormat, "", "--amount=<btc-amount> \\", ""); stream.format(rowFormat, "", "--amount=<btc-amount> \\", "");
stream.format(rowFormat, "", "[--min-amount=<min-btc-amount>] \\", ""); stream.format(rowFormat, "", "[--min-amount=<min-btc-amount>] \\", "");
stream.format(rowFormat, "", "--fixed-price=<price> | --market-price=margin=<percent> \\", ""); stream.format(rowFormat, "", "--fixed-price=<price> | --market-price-margin=<percent> \\", "");
stream.format(rowFormat, "", "--security-deposit=<percent> \\", ""); stream.format(rowFormat, "", "--security-deposit=<percent> \\", "");
stream.format(rowFormat, "", "[--fee-currency=<btc>]", ""); stream.format(rowFormat, "", "[--fee-currency=<bsq|btc>]", "");
stream.format(rowFormat, "", "[--trigger-price=<price>]", "");
stream.format(rowFormat, "", "[--swap=<true|false>]", "");
stream.println();
stream.format(rowFormat, editoffer.name(), "--offer-id=<offer-id> \\", "Edit offer with id");
stream.format(rowFormat, "", "[--fixed-price=<price>] \\", "");
stream.format(rowFormat, "", "[--market-price-margin=<percent>] \\", "");
stream.format(rowFormat, "", "[--trigger-price=<price>] \\", "");
stream.format(rowFormat, "", "[--enabled=<true|false>]", "");
stream.println(); stream.println();
stream.format(rowFormat, canceloffer.name(), "--offer-id=<offer-id>", "Cancel offer with id"); stream.format(rowFormat, canceloffer.name(), "--offer-id=<offer-id>", "Cancel offer with id");
stream.println(); stream.println();
@ -709,22 +737,28 @@ public class CliMain {
stream.format(rowFormat, "", "--currency-code=<currency-code>", ""); stream.format(rowFormat, "", "--currency-code=<currency-code>", "");
stream.println(); stream.println();
stream.format(rowFormat, takeoffer.name(), "--offer-id=<offer-id> \\", "Take offer with id"); stream.format(rowFormat, takeoffer.name(), "--offer-id=<offer-id> \\", "Take offer with id");
stream.format(rowFormat, "", "--payment-account=<payment-account-id>", ""); stream.format(rowFormat, "", "[--payment-account=<payment-account-id>]", "");
stream.format(rowFormat, "", "[--fee-currency=<btc>]", ""); stream.format(rowFormat, "", "[--fee-currency=<btc|bsq>]", "");
stream.println(); stream.println();
stream.format(rowFormat, gettrade.name(), "--trade-id=<trade-id> \\", "Get trade summary or full contract"); stream.format(rowFormat, gettrade.name(), "--trade-id=<trade-id> \\", "Get trade summary or full contract");
stream.format(rowFormat, "", "[--show-contract=<true|false>]", ""); stream.format(rowFormat, "", "[--show-contract=<true|false>]", "");
stream.println(); stream.println();
stream.format(rowFormat, gettrades.name(), "[--category=<open|closed|failed>]", "Get open (default), closed, or failed trades");
stream.println();
stream.format(rowFormat, confirmpaymentstarted.name(), "--trade-id=<trade-id>", "Confirm payment started"); stream.format(rowFormat, confirmpaymentstarted.name(), "--trade-id=<trade-id>", "Confirm payment started");
stream.println(); stream.println();
stream.format(rowFormat, confirmpaymentreceived.name(), "--trade-id=<trade-id>", "Confirm payment received"); stream.format(rowFormat, confirmpaymentreceived.name(), "--trade-id=<trade-id>", "Confirm payment received");
stream.println(); stream.println();
stream.format(rowFormat, keepfunds.name(), "--trade-id=<trade-id>", "Keep received funds in Bisq wallet"); stream.format(rowFormat, closetrade.name(), "--trade-id=<trade-id>", "Close completed trade");
stream.println(); stream.println();
stream.format(rowFormat, withdrawfunds.name(), "--trade-id=<trade-id> --address=<btc-address> \\", stream.format(rowFormat, withdrawfunds.name(), "--trade-id=<trade-id> --address=<btc-address> \\",
"Withdraw received funds to external wallet address"); "Withdraw received trade funds to external wallet address");
stream.format(rowFormat, "", "[--memo=<\"memo\">]", ""); stream.format(rowFormat, "", "[--memo=<\"memo\">]", "");
stream.println(); stream.println();
stream.format(rowFormat, failtrade.name(), "--trade-id=<trade-id>", "Change open trade to failed trade");
stream.println();
stream.format(rowFormat, unfailtrade.name(), "--trade-id=<trade-id>", "Change failed trade to open trade");
stream.println();
stream.format(rowFormat, getpaymentmethods.name(), "", "Get list of supported payment account method ids"); stream.format(rowFormat, getpaymentmethods.name(), "", "Get list of supported payment account method ids");
stream.println(); stream.println();
stream.format(rowFormat, getpaymentacctform.name(), "--payment-method-id=<payment-method-id>", "Get a new payment account form"); stream.format(rowFormat, getpaymentacctform.name(), "--payment-method-id=<payment-method-id>", "Get a new payment account form");
@ -732,8 +766,8 @@ public class CliMain {
stream.format(rowFormat, createpaymentacct.name(), "--payment-account-form=<path>", "Create a new payment account"); stream.format(rowFormat, createpaymentacct.name(), "--payment-account-form=<path>", "Create a new payment account");
stream.println(); stream.println();
stream.format(rowFormat, createcryptopaymentacct.name(), "--account-name=<name> \\", "Create a new cryptocurrency payment account"); stream.format(rowFormat, createcryptopaymentacct.name(), "--account-name=<name> \\", "Create a new cryptocurrency payment account");
stream.format(rowFormat, "", "--currency-code=<btc> \\", ""); stream.format(rowFormat, "", "--currency-code=<bsq> \\", "");
stream.format(rowFormat, "", "--address=<address>", ""); stream.format(rowFormat, "", "--address=<bsq-address>", "");
stream.format(rowFormat, "", "--trade-instant=<true|false>", ""); stream.format(rowFormat, "", "--trade-instant=<true|false>", "");
stream.println(); stream.println();
stream.format(rowFormat, getpaymentaccts.name(), "", "Get user payment accounts"); stream.format(rowFormat, getpaymentaccts.name(), "", "Get user payment accounts");

View file

@ -20,14 +20,15 @@ package bisq.cli;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
class CryptoCurrencyUtil { public class CryptoCurrencyUtil {
public static boolean isSupportedCryptoCurrency(String currencyCode) { public static boolean apiDoesSupportCryptoCurrency(String currencyCode) {
return getSupportedCryptoCurrencies().contains(currencyCode.toUpperCase()); return getSupportedCryptoCurrencies().contains(currencyCode.toUpperCase());
} }
public static List<String> getSupportedCryptoCurrencies() { public static List<String> getSupportedCryptoCurrencies() {
final List<String> result = new ArrayList<>(); final List<String> result = new ArrayList<>();
result.add("BCH");
result.sort(String::compareTo); result.sort(String::compareTo);
return result; return result;
} }

View file

@ -22,10 +22,10 @@ import bisq.proto.grpc.TxFeeRateInfo;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Locale; import java.util.Locale;
@ -33,32 +33,48 @@ import static java.lang.String.format;
import static java.math.RoundingMode.HALF_UP; import static java.math.RoundingMode.HALF_UP;
import static java.math.RoundingMode.UNNECESSARY; import static java.math.RoundingMode.UNNECESSARY;
/**
* Utility for formatting amounts, volumes and fees; there is no i18n support in the CLI.
import monero.common.MoneroUtils; */
@VisibleForTesting @VisibleForTesting
public class CurrencyFormat { public class CurrencyFormat {
private static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); // Use the US locale as a base for all DecimalFormats, but commas should be omitted from number strings.
private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US);
// Use the US locale as a base for all NumberFormats, but commas should be omitted from number strings.
private static final NumberFormat US_LOCALE_NUMBER_FORMAT = NumberFormat.getInstance(Locale.US);
// Formats numbers for internal use, i.e., grpc request parameters.
private static final DecimalFormat INTERNAL_FIAT_DECIMAL_FORMAT = new DecimalFormat("##############0.0000");
static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100_000_000); static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100_000_000);
static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); static final DecimalFormat SATOSHI_FORMAT = new DecimalFormat("###,##0.00000000", DECIMAL_FORMAT_SYMBOLS);
static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,###,##0"); static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.########", DECIMAL_FORMAT_SYMBOLS);
static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,###,##0", DECIMAL_FORMAT_SYMBOLS);
static final BigDecimal SECURITY_DEPOSIT_MULTIPLICAND = new BigDecimal("0.01"); static final BigDecimal BSQ_SATOSHI_DIVISOR = new BigDecimal(100);
static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00", DECIMAL_FORMAT_SYMBOLS);
// TODO: (woodser): replace formatSatoshis(), formatBsq() with formatXmr() public static String formatSatoshis(String sats) {
//noinspection BigDecimalMethodWithoutRoundingCalled
return SATOSHI_FORMAT.format(new BigDecimal(sats).divide(SATOSHI_DIVISOR));
}
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
public static String formatSatoshis(long sats) { public static String formatSatoshis(long sats) {
return BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR)); return SATOSHI_FORMAT.format(new BigDecimal(sats).divide(SATOSHI_DIVISOR));
} }
public static String formatXmr(BigInteger amount) { @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
return "" + MoneroUtils.atomicUnitsToXmr(amount); public static String formatBtc(long sats) {
return BTC_FORMAT.format(new BigDecimal(sats).divide(SATOSHI_DIVISOR));
} }
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
public static String formatBsq(long sats) {
return BSQ_FORMAT.format(new BigDecimal(sats).divide(BSQ_SATOSHI_DIVISOR));
}
public static String formatTxFeeRateInfo(TxFeeRateInfo txFeeRateInfo) { public static String formatTxFeeRateInfo(TxFeeRateInfo txFeeRateInfo) {
if (txFeeRateInfo.getUseCustomTxFeeRate()) if (txFeeRateInfo.getUseCustomTxFeeRate())
@ -72,56 +88,30 @@ public class CurrencyFormat {
formatFeeSatoshis(txFeeRateInfo.getMinFeeServiceRate())); formatFeeSatoshis(txFeeRateInfo.getMinFeeServiceRate()));
} }
public static String formatAmountRange(long minAmount, long amount) { public static String formatInternalFiatPrice(BigDecimal price) {
return minAmount != amount INTERNAL_FIAT_DECIMAL_FORMAT.setMinimumFractionDigits(4);
? formatSatoshis(minAmount) + " - " + formatSatoshis(amount) INTERNAL_FIAT_DECIMAL_FORMAT.setMaximumFractionDigits(4);
: formatSatoshis(amount); return INTERNAL_FIAT_DECIMAL_FORMAT.format(price);
} }
public static String formatVolumeRange(long minVolume, long volume) { public static String formatInternalFiatPrice(double price) {
return minVolume != volume US_LOCALE_NUMBER_FORMAT.setMinimumFractionDigits(4);
? formatOfferVolume(minVolume) + " - " + formatOfferVolume(volume) US_LOCALE_NUMBER_FORMAT.setMaximumFractionDigits(4);
: formatOfferVolume(volume); return US_LOCALE_NUMBER_FORMAT.format(price);
}
public static String formatCryptoCurrencyVolumeRange(long minVolume, long volume) {
return minVolume != volume
? formatCryptoCurrencyOfferVolume(minVolume) + " - " + formatCryptoCurrencyOfferVolume(volume)
: formatCryptoCurrencyOfferVolume(volume);
}
public static String formatMarketPrice(double price) {
NUMBER_FORMAT.setMinimumFractionDigits(4);
NUMBER_FORMAT.setMaximumFractionDigits(4);
return NUMBER_FORMAT.format(price);
} }
public static String formatPrice(long price) { public static String formatPrice(long price) {
NUMBER_FORMAT.setMinimumFractionDigits(4); US_LOCALE_NUMBER_FORMAT.setMinimumFractionDigits(4);
NUMBER_FORMAT.setMaximumFractionDigits(4); US_LOCALE_NUMBER_FORMAT.setMaximumFractionDigits(4);
NUMBER_FORMAT.setRoundingMode(UNNECESSARY); US_LOCALE_NUMBER_FORMAT.setRoundingMode(UNNECESSARY);
return NUMBER_FORMAT.format((double) price / 10_000); return US_LOCALE_NUMBER_FORMAT.format((double) price / 10_000);
} }
public static String formatCryptoCurrencyPrice(long price) { public static String formatFiatVolume(long volume) {
NUMBER_FORMAT.setMinimumFractionDigits(8); US_LOCALE_NUMBER_FORMAT.setMinimumFractionDigits(0);
NUMBER_FORMAT.setMaximumFractionDigits(8); US_LOCALE_NUMBER_FORMAT.setMaximumFractionDigits(0);
NUMBER_FORMAT.setRoundingMode(UNNECESSARY); US_LOCALE_NUMBER_FORMAT.setRoundingMode(HALF_UP);
return NUMBER_FORMAT.format((double) price / SATOSHI_DIVISOR.doubleValue()); return US_LOCALE_NUMBER_FORMAT.format((double) volume / 10_000);
}
public static String formatOfferVolume(long volume) {
NUMBER_FORMAT.setMinimumFractionDigits(0);
NUMBER_FORMAT.setMaximumFractionDigits(0);
NUMBER_FORMAT.setRoundingMode(HALF_UP);
return NUMBER_FORMAT.format((double) volume / 10_000);
}
public static String formatCryptoCurrencyOfferVolume(long volume) {
NUMBER_FORMAT.setMinimumFractionDigits(2);
NUMBER_FORMAT.setMaximumFractionDigits(2);
NUMBER_FORMAT.setRoundingMode(HALF_UP);
return NUMBER_FORMAT.format((double) volume / SATOSHI_DIVISOR.doubleValue());
} }
public static long toSatoshis(String btc) { public static long toSatoshis(String btc) {
@ -135,15 +125,6 @@ public class CurrencyFormat {
} }
} }
public static double toSecurityDepositAsPct(String securityDepositInput) {
try {
return new BigDecimal(securityDepositInput)
.multiply(SECURITY_DEPOSIT_MULTIPLICAND).doubleValue();
} catch (NumberFormatException e) {
throw new IllegalArgumentException(format("'%s' is not a number", securityDepositInput));
}
}
public static String formatFeeSatoshis(long sats) { public static String formatFeeSatoshis(long sats) {
return BTC_TX_FEE_FORMAT.format(BigDecimal.valueOf(sats)); return BTC_TX_FEE_FORMAT.format(BigDecimal.valueOf(sats));
} }

View file

@ -24,8 +24,8 @@ import java.util.function.Function;
import static bisq.cli.ColumnHeaderConstants.COL_HEADER_DIRECTION; import static bisq.cli.ColumnHeaderConstants.COL_HEADER_DIRECTION;
import static java.lang.String.format; import static java.lang.String.format;
import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferDirection.BUY;
import static protobuf.OfferPayload.Direction.SELL; import static protobuf.OfferDirection.SELL;
class DirectionFormat { class DirectionFormat {

View file

@ -20,62 +20,28 @@ package bisq.cli;
import bisq.proto.grpc.AddressBalanceInfo; import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.BalancesInfo; import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.BtcBalanceInfo; import bisq.proto.grpc.BtcBalanceInfo;
import bisq.proto.grpc.CancelOfferRequest;
import bisq.proto.grpc.ConfirmPaymentReceivedRequest;
import bisq.proto.grpc.ConfirmPaymentStartedRequest;
import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetMethodHelpRequest; import bisq.proto.grpc.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.GetTradesRequest; import bisq.proto.grpc.GetTradesRequest;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetVersionRequest; 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.OfferInfo;
import bisq.proto.grpc.RegisterDisputeAgentRequest; import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.StopRequest; import bisq.proto.grpc.StopRequest;
import bisq.proto.grpc.TakeOfferReply;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TradeInfo; import bisq.proto.grpc.TradeInfo;
import bisq.proto.grpc.TxFeeRateInfo; import bisq.proto.grpc.TxFeeRateInfo;
import bisq.proto.grpc.TxInfo; import bisq.proto.grpc.TxInfo;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WithdrawFundsRequest;
import bisq.proto.grpc.XmrBalanceInfo;
import protobuf.PaymentAccount; import protobuf.PaymentAccount;
import protobuf.PaymentMethod; import protobuf.PaymentMethod;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static bisq.cli.CryptoCurrencyUtil.isSupportedCryptoCurrency;
import static java.util.Comparator.comparing; import bisq.cli.request.OffersServiceRequest;
import static java.util.stream.Collectors.toList; import bisq.cli.request.PaymentAccountsServiceRequest;
import static protobuf.OfferPayload.Direction.BUY; import bisq.cli.request.TradesServiceRequest;
import static protobuf.OfferPayload.Direction.SELL; import bisq.cli.request.WalletsServiceRequest;
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
@ -83,9 +49,19 @@ import static protobuf.OfferPayload.Direction.SELL;
public final class GrpcClient { public final class GrpcClient {
private final GrpcStubs grpcStubs; private final GrpcStubs grpcStubs;
private final OffersServiceRequest offersServiceRequest;
private final TradesServiceRequest tradesServiceRequest;
private final WalletsServiceRequest walletsServiceRequest;
private final PaymentAccountsServiceRequest paymentAccountsServiceRequest;
public GrpcClient(String apiHost, int apiPort, String apiPassword) { public GrpcClient(String apiHost,
int apiPort,
String apiPassword) {
this.grpcStubs = new GrpcStubs(apiHost, apiPort, apiPassword); this.grpcStubs = new GrpcStubs(apiHost, apiPort, apiPassword);
this.offersServiceRequest = new OffersServiceRequest(grpcStubs);
this.tradesServiceRequest = new TradesServiceRequest(grpcStubs);
this.walletsServiceRequest = new WalletsServiceRequest(grpcStubs);
this.paymentAccountsServiceRequest = new PaymentAccountsServiceRequest(grpcStubs);
} }
public String getVersion() { public String getVersion() {
@ -94,86 +70,51 @@ public final class GrpcClient {
} }
public BalancesInfo getBalances() { public BalancesInfo getBalances() {
return getBalances(""); return walletsServiceRequest.getBalances();
} }
public BtcBalanceInfo getBtcBalances() { public BtcBalanceInfo getBtcBalances() {
return getBalances("BTC").getBtc(); return walletsServiceRequest.getBtcBalances();
}
public XmrBalanceInfo getXmrBalances() {
return getBalances("XMR").getXmr();
} }
public BalancesInfo getBalances(String currencyCode) { public BalancesInfo getBalances(String currencyCode) {
var request = GetBalancesRequest.newBuilder() return walletsServiceRequest.getBalances(currencyCode);
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.walletsService.getBalances(request).getBalances();
} }
public AddressBalanceInfo getAddressBalance(String address) { public AddressBalanceInfo getAddressBalance(String address) {
var request = GetAddressBalanceRequest.newBuilder() return walletsServiceRequest.getAddressBalance(address);
.setAddress(address).build();
return grpcStubs.walletsService.getAddressBalance(request).getAddressBalanceInfo();
} }
public double getBtcPrice(String currencyCode) { public double getBtcPrice(String currencyCode) {
var request = MarketPriceRequest.newBuilder() return walletsServiceRequest.getBtcPrice(currencyCode);
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.priceService.getMarketPrice(request).getPrice();
} }
public List<AddressBalanceInfo> getFundingAddresses() { public List<AddressBalanceInfo> getFundingAddresses() {
var request = GetFundingAddressesRequest.newBuilder().build(); return walletsServiceRequest.getFundingAddresses();
return grpcStubs.walletsService.getFundingAddresses(request).getAddressBalanceInfoList();
} }
public String getUnusedBtcAddress() { public String getUnusedBtcAddress() {
var request = GetFundingAddressesRequest.newBuilder().build(); return walletsServiceRequest.getUnusedBtcAddress();
var addressBalances = grpcStubs.walletsService.getFundingAddresses(request)
.getAddressBalanceInfoList();
//noinspection OptionalGetWithoutIsPresent
return addressBalances.stream()
.filter(AddressBalanceInfo::getIsAddressUnused)
.findFirst()
.get()
.getAddress();
} }
public TxInfo sendBtc(String address, String amount, String txFeeRate, String memo) { public TxInfo sendBtc(String address, String amount, String txFeeRate, String memo) {
var request = SendBtcRequest.newBuilder() return walletsServiceRequest.sendBtc(address, amount, txFeeRate, memo);
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.setMemo(memo)
.build();
return grpcStubs.walletsService.sendBtc(request).getTxInfo();
} }
public TxFeeRateInfo getTxFeeRate() { public TxFeeRateInfo getTxFeeRate() {
var request = GetTxFeeRateRequest.newBuilder().build(); return walletsServiceRequest.getTxFeeRate();
return grpcStubs.walletsService.getTxFeeRate(request).getTxFeeRateInfo();
} }
public TxFeeRateInfo setTxFeeRate(long txFeeRate) { public TxFeeRateInfo setTxFeeRate(long txFeeRate) {
var request = SetTxFeeRatePreferenceRequest.newBuilder() return walletsServiceRequest.setTxFeeRate(txFeeRate);
.setTxFeeRatePreference(txFeeRate)
.build();
return grpcStubs.walletsService.setTxFeeRatePreference(request).getTxFeeRateInfo();
} }
public TxFeeRateInfo unsetTxFeeRate() { public TxFeeRateInfo unsetTxFeeRate() {
var request = UnsetTxFeeRatePreferenceRequest.newBuilder().build(); return walletsServiceRequest.unsetTxFeeRate();
return grpcStubs.walletsService.unsetTxFeeRatePreference(request).getTxFeeRateInfo();
} }
public TxInfo getTransaction(String txId) { public TxInfo getTransaction(String txId) {
var request = GetTransactionRequest.newBuilder() return walletsServiceRequest.getTransaction(txId);
.setTxId(txId)
.build();
return grpcStubs.walletsService.getTransaction(request).getTxInfo();
} }
public OfferInfo createFixedPricedOffer(String direction, public OfferInfo createFixedPricedOffer(String direction,
@ -181,35 +122,38 @@ public final class GrpcClient {
long amount, long amount,
long minAmount, long minAmount,
String fixedPrice, String fixedPrice,
double securityDeposit, double securityDepositPct,
String paymentAcctId) { String paymentAcctId) {
return createOffer(direction, return offersServiceRequest.createOffer(direction,
currencyCode, currencyCode,
amount, amount,
minAmount, minAmount,
false, false,
fixedPrice, fixedPrice,
0.00, 0.00,
securityDeposit, securityDepositPct,
paymentAcctId); paymentAcctId,
"0" /* no trigger price */);
} }
public OfferInfo createMarketBasedPricedOffer(String direction, public OfferInfo createMarketBasedPricedOffer(String direction,
String currencyCode, String currencyCode,
long amount, long amount,
long minAmount, long minAmount,
double marketPriceMargin, double marketPriceMarginPct,
double securityDeposit, double securityDepositPct,
String paymentAcctId) { String paymentAcctId,
return createOffer(direction, String triggerPrice) {
return offersServiceRequest.createOffer(direction,
currencyCode, currencyCode,
amount, amount,
minAmount, minAmount,
true, true,
"0", "0",
marketPriceMargin, marketPriceMarginPct,
securityDeposit, securityDepositPct,
paymentAcctId); paymentAcctId,
triggerPrice);
} }
public OfferInfo createOffer(String direction, public OfferInfo createOffer(String direction,
@ -218,244 +162,140 @@ public final class GrpcClient {
long minAmount, long minAmount,
boolean useMarketBasedPrice, boolean useMarketBasedPrice,
String fixedPrice, String fixedPrice,
double marketPriceMargin, double marketPriceMarginPct,
double securityDeposit, double securityDepositPct,
String paymentAcctId) { String paymentAcctId,
var request = CreateOfferRequest.newBuilder() String triggerPrice) {
.setDirection(direction) return offersServiceRequest.createOffer(direction,
.setCurrencyCode(currencyCode) currencyCode,
.setAmount(amount) amount,
.setMinAmount(minAmount) minAmount,
.setUseMarketBasedPrice(useMarketBasedPrice) useMarketBasedPrice,
.setPrice(fixedPrice) fixedPrice,
.setMarketPriceMargin(marketPriceMargin) marketPriceMarginPct,
.setBuyerSecurityDeposit(securityDeposit) securityDepositPct,
.setPaymentAccountId(paymentAcctId) paymentAcctId,
.build(); triggerPrice);
return grpcStubs.offersService.createOffer(request).getOffer();
} }
public void cancelOffer(String offerId) { public void cancelOffer(String offerId) {
var request = CancelOfferRequest.newBuilder() offersServiceRequest.cancelOffer(offerId);
.setId(offerId)
.build();
grpcStubs.offersService.cancelOffer(request);
} }
public OfferInfo getOffer(String offerId) { public OfferInfo getOffer(String offerId) {
var request = GetOfferRequest.newBuilder() return offersServiceRequest.getOffer(offerId);
.setId(offerId)
.build();
return grpcStubs.offersService.getOffer(request).getOffer();
} }
@Deprecated // Since 5-Dec-2021.
// Endpoint to be removed from future version. Use getOffer service method instead.
public OfferInfo getMyOffer(String offerId) { public OfferInfo getMyOffer(String offerId) {
var request = GetMyOfferRequest.newBuilder() return offersServiceRequest.getMyOffer(offerId);
.setId(offerId)
.build();
return grpcStubs.offersService.getMyOffer(request).getOffer();
} }
public List<OfferInfo> getOffers(String direction, String currencyCode) { public List<OfferInfo> getOffers(String direction, String currencyCode) {
if (isSupportedCryptoCurrency(currencyCode)) { return offersServiceRequest.getOffers(direction, currencyCode);
return getCryptoCurrencyOffers(direction, currencyCode);
} else {
var request = GetOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.offersService.getOffers(request).getOffersList();
}
}
public List<OfferInfo> getCryptoCurrencyOffers(String direction, String currencyCode) {
return getOffers(direction, "XMR").stream()
.filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode))
.collect(toList());
} }
public List<OfferInfo> getOffersSortedByDate(String currencyCode) { public List<OfferInfo> getOffersSortedByDate(String currencyCode) {
ArrayList<OfferInfo> offers = new ArrayList<>(); return offersServiceRequest.getOffersSortedByDate(currencyCode);
offers.addAll(getOffers(BUY.name(), currencyCode));
offers.addAll(getOffers(SELL.name(), currencyCode));
return sortOffersByDate(offers);
} }
public List<OfferInfo> getOffersSortedByDate(String direction, String currencyCode) { public List<OfferInfo> getOffersSortedByDate(String direction, String currencyCode) {
var offers = getOffers(direction, currencyCode); return offersServiceRequest.getOffersSortedByDate(direction, currencyCode);
return offers.isEmpty() ? offers : sortOffersByDate(offers);
} }
public List<OfferInfo> getMyOffers(String direction, String currencyCode) { public List<OfferInfo> getMyOffers(String direction, String currencyCode) {
if (isSupportedCryptoCurrency(currencyCode)) { return offersServiceRequest.getMyOffers(direction, currencyCode);
return getMyCryptoCurrencyOffers(direction, currencyCode);
} else {
var request = GetMyOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.offersService.getMyOffers(request).getOffersList();
}
}
public List<OfferInfo> getMyCryptoCurrencyOffers(String direction, String currencyCode) {
return getMyOffers(direction, "BTC").stream()
.filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode))
.collect(toList());
}
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) { public List<OfferInfo> getMyOffersSortedByDate(String currencyCode) {
ArrayList<OfferInfo> offers = new ArrayList<>(); return offersServiceRequest.getMyOffersSortedByDate(currencyCode);
offers.addAll(getMyOffers(BUY.name(), currencyCode));
offers.addAll(getMyOffers(SELL.name(), currencyCode));
return sortOffersByDate(offers);
} }
public OfferInfo getMostRecentOffer(String direction, String currencyCode) { public List<OfferInfo> getMyOffersSortedByDate(String direction, String currencyCode) {
List<OfferInfo> offers = getOffersSortedByDate(direction, currencyCode); return offersServiceRequest.getMyOffersSortedByDate(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(toList());
}
public TakeOfferReply getTakeOfferReply(String offerId, String paymentAccountId) {
var request = TakeOfferRequest.newBuilder()
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId)
.build();
return grpcStubs.tradesService.takeOffer(request);
} }
public TradeInfo takeOffer(String offerId, String paymentAccountId) { public TradeInfo takeOffer(String offerId, String paymentAccountId) {
var reply = getTakeOfferReply(offerId, paymentAccountId); return tradesServiceRequest.takeOffer(offerId, paymentAccountId);
if (reply.hasTrade())
return reply.getTrade();
else
throw new IllegalStateException(reply.getFailureReason().getDescription());
} }
public TradeInfo getTrade(String tradeId) { public TradeInfo getTrade(String tradeId) {
var request = GetTradeRequest.newBuilder() return tradesServiceRequest.getTrade(tradeId);
.setTradeId(tradeId)
.build();
return grpcStubs.tradesService.getTrade(request).getTrade();
} }
public List<TradeInfo> getTrades() { public List<TradeInfo> getOpenTrades() {
var request = GetTradesRequest.newBuilder().build(); return tradesServiceRequest.getOpenTrades();
return grpcStubs.tradesService.getTrades(request).getTradesList(); }
public List<TradeInfo> getTradeHistory(GetTradesRequest.Category category) {
return tradesServiceRequest.getTradeHistory(category);
} }
public void confirmPaymentStarted(String tradeId) { public void confirmPaymentStarted(String tradeId) {
var request = ConfirmPaymentStartedRequest.newBuilder() tradesServiceRequest.confirmPaymentStarted(tradeId);
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.confirmPaymentStarted(request);
} }
public void confirmPaymentReceived(String tradeId) { public void confirmPaymentReceived(String tradeId) {
var request = ConfirmPaymentReceivedRequest.newBuilder() tradesServiceRequest.confirmPaymentReceived(tradeId);
.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) { public void withdrawFunds(String tradeId, String address, String memo) {
var request = WithdrawFundsRequest.newBuilder() tradesServiceRequest.withdrawFunds(tradeId, address, memo);
.setTradeId(tradeId)
.setAddress(address)
.setMemo(memo)
.build();
grpcStubs.tradesService.withdrawFunds(request);
} }
public List<PaymentMethod> getPaymentMethods() { public List<PaymentMethod> getPaymentMethods() {
var request = GetPaymentMethodsRequest.newBuilder().build(); return paymentAccountsServiceRequest.getPaymentMethods();
return grpcStubs.paymentAccountsService.getPaymentMethods(request).getPaymentMethodsList();
} }
public String getPaymentAcctFormAsJson(String paymentMethodId) { public String getPaymentAcctFormAsJson(String paymentMethodId) {
var request = GetPaymentAccountFormRequest.newBuilder() return paymentAccountsServiceRequest.getPaymentAcctFormAsJson(paymentMethodId);
.setPaymentMethodId(paymentMethodId)
.build();
return grpcStubs.paymentAccountsService.getPaymentAccountForm(request).getPaymentAccountFormJson();
} }
public PaymentAccount createPaymentAccount(String json) { public PaymentAccount createPaymentAccount(String json) {
var request = CreatePaymentAccountRequest.newBuilder() return paymentAccountsServiceRequest.createPaymentAccount(json);
.setPaymentAccountForm(json)
.build();
return grpcStubs.paymentAccountsService.createPaymentAccount(request).getPaymentAccount();
} }
public List<PaymentAccount> getPaymentAccounts() { public List<PaymentAccount> getPaymentAccounts() {
var request = GetPaymentAccountsRequest.newBuilder().build(); return paymentAccountsServiceRequest.getPaymentAccounts();
return grpcStubs.paymentAccountsService.getPaymentAccounts(request).getPaymentAccountsList(); }
public PaymentAccount getPaymentAccount(String accountName) {
return paymentAccountsServiceRequest.getPaymentAccount(accountName);
} }
public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
String currencyCode, String currencyCode,
String address, String address,
boolean tradeInstant) { boolean tradeInstant) {
var request = CreateCryptoCurrencyPaymentAccountRequest.newBuilder() return paymentAccountsServiceRequest.createCryptoCurrencyPaymentAccount(accountName,
.setAccountName(accountName) currencyCode,
.setCurrencyCode(currencyCode) address,
.setAddress(address) tradeInstant);
.setTradeInstant(tradeInstant)
.build();
return grpcStubs.paymentAccountsService.createCryptoCurrencyPaymentAccount(request).getPaymentAccount();
} }
public List<PaymentMethod> getCryptoPaymentMethods() { public List<PaymentMethod> getCryptoPaymentMethods() {
var request = GetCryptoCurrencyPaymentMethodsRequest.newBuilder().build(); return paymentAccountsServiceRequest.getCryptoPaymentMethods();
return grpcStubs.paymentAccountsService.getCryptoCurrencyPaymentMethods(request).getPaymentMethodsList();
} }
public void lockWallet() { public void lockWallet() {
var request = LockWalletRequest.newBuilder().build(); walletsServiceRequest.lockWallet();
grpcStubs.walletsService.lockWallet(request);
} }
public void unlockWallet(String walletPassword, long timeout) { public void unlockWallet(String walletPassword, long timeout) {
var request = UnlockWalletRequest.newBuilder() walletsServiceRequest.unlockWallet(walletPassword, timeout);
.setPassword(walletPassword)
.setTimeout(timeout).build();
grpcStubs.walletsService.unlockWallet(request);
} }
public void removeWalletPassword(String walletPassword) { public void removeWalletPassword(String walletPassword) {
var request = RemoveWalletPasswordRequest.newBuilder() walletsServiceRequest.removeWalletPassword(walletPassword);
.setPassword(walletPassword).build();
grpcStubs.walletsService.removeWalletPassword(request);
} }
public void setWalletPassword(String walletPassword) { public void setWalletPassword(String walletPassword) {
var request = SetWalletPasswordRequest.newBuilder() walletsServiceRequest.setWalletPassword(walletPassword);
.setPassword(walletPassword).build();
grpcStubs.walletsService.setWalletPassword(request);
} }
public void setWalletPassword(String oldWalletPassword, String newWalletPassword) { public void setWalletPassword(String oldWalletPassword, String newWalletPassword) {
var request = SetWalletPasswordRequest.newBuilder() walletsServiceRequest.setWalletPassword(oldWalletPassword, newWalletPassword);
.setPassword(oldWalletPassword)
.setNewPassword(newWalletPassword).build();
grpcStubs.walletsService.setWalletPassword(request);
} }
public void registerDisputeAgent(String disputeAgentType, String registrationKey) { public void registerDisputeAgent(String disputeAgentType, String registrationKey) {

View file

@ -22,16 +22,19 @@ package bisq.cli;
*/ */
public enum Method { public enum Method {
canceloffer, canceloffer,
closetrade,
confirmpaymentreceived, confirmpaymentreceived,
confirmpaymentstarted, confirmpaymentstarted,
createoffer, createoffer,
editoffer,
createpaymentacct, createpaymentacct,
createcryptopaymentacct, createcryptopaymentacct,
getaddressbalance, getaddressbalance,
getbalance, getbalance,
getbtcprice, getbtcprice,
getfundingaddresses, getfundingaddresses,
getmyoffer, @Deprecated // Since 27-Dec-2021.
getmyoffer, // Endpoint to be removed from future version. Use getoffer instead.
getmyoffers, getmyoffers,
getoffer, getoffer,
getoffers, getoffers,
@ -39,10 +42,13 @@ public enum Method {
getpaymentaccts, getpaymentaccts,
getpaymentmethods, getpaymentmethods,
gettrade, gettrade,
gettrades,
failtrade,
unfailtrade,
gettransaction, gettransaction,
gettxfeerate, gettxfeerate,
getunusedbsqaddress,
getversion, getversion,
keepfunds,
lockwallet, lockwallet,
registerdisputeagent, registerdisputeagent,
removewalletpassword, removewalletpassword,

View file

@ -1,270 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. 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.BtcBalanceInfo;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.XmrBalanceInfo;
import protobuf.PaymentAccount;
import com.google.common.annotations.VisibleForTesting;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.stream.Collectors;
import static bisq.cli.ColumnHeaderConstants.*;
import static bisq.cli.CurrencyFormat.*;
import static bisq.cli.DirectionFormat.directionFormat;
import static bisq.cli.DirectionFormat.getLongestDirectionColWidth;
import static com.google.common.base.Strings.padEnd;
import static com.google.common.base.Strings.padStart;
import static java.lang.String.format;
import static java.util.Collections.max;
import static java.util.Comparator.comparing;
import static java.util.TimeZone.getTimeZone;
@VisibleForTesting
public class TableFormat {
static final TimeZone TZ_UTC = getTimeZone("UTC");
static final SimpleDateFormat DATE_FORMAT_ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
public static String formatAddressBalanceTbl(List<AddressBalanceInfo> addressBalanceInfo) {
String headerFormatString = COL_HEADER_ADDRESS + COL_HEADER_DELIMITER
+ COL_HEADER_AVAILABLE_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_CONFIRMATIONS + COL_HEADER_DELIMITER
+ COL_HEADER_IS_USED_ADDRESS + COL_HEADER_DELIMITER + "\n";
String headerLine = format(headerFormatString, "XMR");
String colDataFormat = "%-" + COL_HEADER_ADDRESS.length() + "s" // lt justify
+ " %" + (COL_HEADER_AVAILABLE_BALANCE.length() - 1) + "s" // rt justify
+ " %" + COL_HEADER_CONFIRMATIONS.length() + "d" // rt justify
+ " %-" + COL_HEADER_IS_USED_ADDRESS.length() + "s"; // lt justify
return headerLine
+ addressBalanceInfo.stream()
.map(info -> format(colDataFormat,
info.getAddress(),
formatSatoshis(info.getBalance()),
info.getNumConfirmations(),
info.getIsAddressUnused() ? "NO" : "YES"))
.collect(Collectors.joining("\n"));
}
public static String formatBalancesTbls(BalancesInfo balancesInfo) {
return "XMR" + "\n"
+ formatBtcBalanceInfoTbl(balancesInfo.getBtc());
}
public static String formatBtcBalanceInfoTbl(BtcBalanceInfo btcBalanceInfo) {
String headerLine = COL_HEADER_AVAILABLE_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_RESERVED_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_TOTAL_AVAILABLE_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_LOCKED_BALANCE + COL_HEADER_DELIMITER + "\n";
String colDataFormat = "%" + COL_HEADER_AVAILABLE_BALANCE.length() + "s" // rt justify
+ " %" + (COL_HEADER_RESERVED_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_TOTAL_AVAILABLE_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_LOCKED_BALANCE.length() + 1) + "s"; // rt justify
return headerLine + format(colDataFormat,
formatSatoshis(btcBalanceInfo.getAvailableBalance()),
formatSatoshis(btcBalanceInfo.getReservedBalance()),
formatSatoshis(btcBalanceInfo.getTotalAvailableBalance()),
formatSatoshis(btcBalanceInfo.getLockedBalance()));
}
public static String formatXmrBalanceInfoTbl(XmrBalanceInfo xmrBalanceInfo) {
String headerLine = COL_HEADER_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_AVAILABLE_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_LOCKED_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_RESERVED_OFFER_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_RESERVED_TRADE_BALANCE + COL_HEADER_DELIMITER + "\n";
String colDataFormat = "%" + COL_HEADER_BALANCE.length() + "s" // rt justify
+ " %" + (COL_HEADER_AVAILABLE_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_LOCKED_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_RESERVED_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_TOTAL_AVAILABLE_BALANCE.length() + 1) + "s"; // rt justify
return headerLine + format(colDataFormat,
formatSatoshis(xmrBalanceInfo.getUnlockedBalance() + xmrBalanceInfo.getLockedBalance()), // total balance
formatSatoshis(xmrBalanceInfo.getUnlockedBalance()),
formatSatoshis(xmrBalanceInfo.getReservedOfferBalance()),
formatSatoshis(xmrBalanceInfo.getReservedTradeBalance()));
}
public static String formatPaymentAcctTbl(List<PaymentAccount> paymentAccounts) {
// Some column values might be longer than header, so we need to calculate them.
int nameColWidth = getLongestColumnSize(
COL_HEADER_NAME.length(),
paymentAccounts.stream().map(PaymentAccount::getAccountName)
.collect(Collectors.toList()));
int paymentMethodColWidth = getLongestColumnSize(
COL_HEADER_PAYMENT_METHOD.length(),
paymentAccounts.stream().map(a -> a.getPaymentMethod().getId())
.collect(Collectors.toList()));
String headerLine = padEnd(COL_HEADER_NAME, nameColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_CURRENCY + COL_HEADER_DELIMITER
+ padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_UUID + COL_HEADER_DELIMITER + "\n";
String colDataFormat = "%-" + nameColWidth + "s" // left justify
+ " %-" + COL_HEADER_CURRENCY.length() + "s" // left justify
+ " %-" + paymentMethodColWidth + "s" // left justify
+ " %-" + COL_HEADER_UUID.length() + "s"; // left justify
return headerLine
+ paymentAccounts.stream()
.map(a -> format(colDataFormat,
a.getAccountName(),
a.getSelectedTradeCurrency().getCode(),
a.getPaymentMethod().getId(),
a.getId()))
.collect(Collectors.joining("\n"));
}
public static String formatOfferTable(List<OfferInfo> offers, String currencyCode) {
if (offers == null || offers.isEmpty())
throw new IllegalArgumentException(format("%s offers argument is empty", currencyCode.toLowerCase()));
String baseCurrencyCode = offers.get(0).getBaseCurrencyCode();
return baseCurrencyCode.equalsIgnoreCase("XMR")
? formatFiatOfferTable(offers, currencyCode)
: formatCryptoCurrencyOfferTable(offers, baseCurrencyCode);
}
private static String formatFiatOfferTable(List<OfferInfo> offers, String fiatCurrencyCode) {
// Some column values might be longer than header, so we need to calculate them.
int amountColWith = getLongestAmountColWidth(offers);
int volumeColWidth = getLongestVolumeColWidth(offers);
int paymentMethodColWidth = getLongestPaymentMethodColWidth(offers);
String headersFormat = COL_HEADER_DIRECTION + COL_HEADER_DELIMITER
+ COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> fiatCurrencyCode
+ padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER
// COL_HEADER_VOLUME includes %s -> fiatCurrencyCode
+ padStart(COL_HEADER_VOLUME, volumeColWidth, ' ') + COL_HEADER_DELIMITER
+ padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_CREATION_DATE + COL_HEADER_DELIMITER
+ COL_HEADER_UUID.trim() + "%n";
String headerLine = format(headersFormat,
fiatCurrencyCode.toUpperCase(),
fiatCurrencyCode.toUpperCase());
String colDataFormat = "%-" + (COL_HEADER_DIRECTION.length() + COL_HEADER_DELIMITER.length()) + "s"
+ "%" + (COL_HEADER_PRICE.length() - 1) + "s"
+ " %" + amountColWith + "s"
+ " %" + (volumeColWidth - 1) + "s"
+ " %-" + paymentMethodColWidth + "s"
+ " %-" + (COL_HEADER_CREATION_DATE.length()) + "s"
+ " %-" + COL_HEADER_UUID.length() + "s";
return headerLine
+ offers.stream()
.map(o -> format(colDataFormat,
o.getDirection(),
formatPrice(o.getPrice()),
formatAmountRange(o.getMinAmount(), o.getAmount()),
formatVolumeRange(o.getMinVolume(), o.getVolume()),
o.getPaymentMethodShortName(),
formatTimestamp(o.getDate()),
o.getId()))
.collect(Collectors.joining("\n"));
}
private static String formatCryptoCurrencyOfferTable(List<OfferInfo> offers, String cryptoCurrencyCode) {
// Some column values might be longer than header, so we need to calculate them.
int directionColWidth = getLongestDirectionColWidth(offers);
int amountColWith = getLongestAmountColWidth(offers);
int volumeColWidth = getLongestCryptoCurrencyVolumeColWidth(offers);
int paymentMethodColWidth = getLongestPaymentMethodColWidth(offers);
// TODO use memoize function to avoid duplicate the formatting done above?
String headersFormat = padEnd(COL_HEADER_DIRECTION, directionColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_PRICE_OF_ALTCOIN + COL_HEADER_DELIMITER // includes %s -> cryptoCurrencyCode
+ padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER
// COL_HEADER_VOLUME includes %s -> cryptoCurrencyCode
+ padStart(COL_HEADER_VOLUME, volumeColWidth, ' ') + COL_HEADER_DELIMITER
+ padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_CREATION_DATE + COL_HEADER_DELIMITER
+ COL_HEADER_UUID.trim() + "%n";
String headerLine = format(headersFormat,
cryptoCurrencyCode.toUpperCase(),
cryptoCurrencyCode.toUpperCase());
String colDataFormat = "%-" + directionColWidth + "s"
+ "%" + (COL_HEADER_PRICE_OF_ALTCOIN.length() + 1) + "s"
+ " %" + amountColWith + "s"
+ " %" + (volumeColWidth - 1) + "s"
+ " %-" + paymentMethodColWidth + "s"
+ " %-" + (COL_HEADER_CREATION_DATE.length()) + "s"
+ " %-" + COL_HEADER_UUID.length() + "s";
return headerLine
+ offers.stream()
.map(o -> format(colDataFormat,
directionFormat.apply(o),
formatCryptoCurrencyPrice(o.getPrice()),
formatAmountRange(o.getMinAmount(), o.getAmount()),
formatCryptoCurrencyVolumeRange(o.getMinVolume(), o.getVolume()),
o.getPaymentMethodShortName(),
formatTimestamp(o.getDate()),
o.getId()))
.collect(Collectors.joining("\n"));
}
private static int getLongestPaymentMethodColWidth(List<OfferInfo> offers) {
return getLongestColumnSize(
COL_HEADER_PAYMENT_METHOD.length(),
offers.stream()
.map(OfferInfo::getPaymentMethodShortName)
.collect(Collectors.toList()));
}
private static int getLongestAmountColWidth(List<OfferInfo> offers) {
return getLongestColumnSize(
COL_HEADER_AMOUNT.length(),
offers.stream()
.map(o -> formatAmountRange(o.getMinAmount(), o.getAmount()))
.collect(Collectors.toList()));
}
private static int getLongestVolumeColWidth(List<OfferInfo> offers) {
// Pad this col width by 1 space.
return 1 + getLongestColumnSize(
COL_HEADER_VOLUME.length(),
offers.stream()
.map(o -> formatVolumeRange(o.getMinVolume(), o.getVolume()))
.collect(Collectors.toList()));
}
private static int getLongestCryptoCurrencyVolumeColWidth(List<OfferInfo> offers) {
// Pad this col width by 1 space.
return 1 + getLongestColumnSize(
COL_HEADER_VOLUME.length(),
offers.stream()
.map(o -> formatCryptoCurrencyVolumeRange(o.getMinVolume(), o.getVolume()))
.collect(Collectors.toList()));
}
// Return size of the longest string value, or the header.len, whichever is greater.
private static int getLongestColumnSize(int headerLength, List<String> strings) {
int longest = max(strings, comparing(String::length)).length();
return Math.max(longest, headerLength);
}
private static String formatTimestamp(long timestamp) {
DATE_FORMAT_ISO_8601.setTimeZone(TZ_UTC);
return DATE_FORMAT_ISO_8601.format(new Date(timestamp));
}
}

View file

@ -1,177 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import bisq.core.util.ParsingUtils;
import bisq.proto.grpc.ContractInfo;
import bisq.proto.grpc.TradeInfo;
import com.google.common.annotations.VisibleForTesting;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import static bisq.cli.ColumnHeaderConstants.*;
import static bisq.cli.CurrencyFormat.*;
import static com.google.common.base.Strings.padEnd;
@VisibleForTesting
public class TradeFormat {
private static final String YES = "YES";
private static final String NO = "NO";
// TODO add String format(List<TradeInfo> trades)
@VisibleForTesting
public static String format(TradeInfo tradeInfo) {
// Some column values might be longer than header, so we need to calculate them.
int shortIdColWidth = Math.max(COL_HEADER_TRADE_SHORT_ID.length(), tradeInfo.getShortId().length());
int roleColWidth = Math.max(COL_HEADER_TRADE_ROLE.length(), tradeInfo.getRole().length());
// We only show taker fee under its header when user is the taker.
boolean isTaker = tradeInfo.getRole().toLowerCase().contains("taker");
Supplier<String> makerFeeHeader = () -> !isTaker ?
COL_HEADER_TRADE_MAKER_FEE + COL_HEADER_DELIMITER
: "";
Supplier<String> makerFeeHeaderSpec = () -> !isTaker ?
"%" + (COL_HEADER_TRADE_MAKER_FEE.length() + 2) + "s"
: "";
Supplier<String> takerFeeHeader = () -> isTaker ?
COL_HEADER_TRADE_TAKER_FEE + COL_HEADER_DELIMITER
: "";
Supplier<String> takerFeeHeaderSpec = () -> isTaker ?
"%" + (COL_HEADER_TRADE_TAKER_FEE.length() + 2) + "s"
: "";
String headersFormat = padEnd(COL_HEADER_TRADE_SHORT_ID, shortIdColWidth, ' ') + COL_HEADER_DELIMITER
+ padEnd(COL_HEADER_TRADE_ROLE, roleColWidth, ' ') + COL_HEADER_DELIMITER
+ priceHeader.apply(tradeInfo) + COL_HEADER_DELIMITER // includes %s -> currencyCode
+ padEnd(COL_HEADER_TRADE_AMOUNT, 12, ' ') + COL_HEADER_DELIMITER
+ padEnd(COL_HEADER_TRADE_TX_FEE, 12, ' ') + COL_HEADER_DELIMITER
+ makerFeeHeader.get()
// maker or taker fee header, not both
+ takerFeeHeader.get()
+ COL_HEADER_TRADE_DEPOSIT_PUBLISHED + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_DEPOSIT_CONFIRMED + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_BUYER_COST + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_PAYMENT_SENT + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_PAYMENT_RECEIVED + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_PAYOUT_PUBLISHED + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_WITHDRAWN + COL_HEADER_DELIMITER
+ "%n";
String counterCurrencyCode = tradeInfo.getOffer().getCounterCurrencyCode();
String baseCurrencyCode = tradeInfo.getOffer().getBaseCurrencyCode();
String headerLine = String.format(headersFormat,
/* COL_HEADER_PRICE */ priceHeaderCurrencyCode.apply(tradeInfo),
/* COL_HEADER_TRADE_AMOUNT */ baseCurrencyCode,
/* COL_HEADER_TRADE_(M||T)AKER_FEE */ makerTakerFeeHeaderCurrencyCode.apply(tradeInfo, isTaker),
/* COL_HEADER_TRADE_BUYER_COST */ counterCurrencyCode,
/* COL_HEADER_TRADE_PAYMENT_SENT */ paymentStatusHeaderCurrencyCode.apply(tradeInfo),
/* COL_HEADER_TRADE_PAYMENT_RECEIVED */ paymentStatusHeaderCurrencyCode.apply(tradeInfo));
String colDataFormat = "%-" + shortIdColWidth + "s" // lt justify
+ " %-" + (roleColWidth + COL_HEADER_DELIMITER.length()) + "s" // left
+ "%" + (COL_HEADER_PRICE.length() - 1) + "s" // rt justify
+ "%" + (COL_HEADER_TRADE_AMOUNT.length() + 1) + "s" // rt justify
+ "%" + (COL_HEADER_TRADE_TX_FEE.length() + 1) + "s" // rt justify
+ makerFeeHeaderSpec.get() // rt justify
// OR (one of them is an empty string)
+ takerFeeHeaderSpec.get() // rt justify
+ " %-" + COL_HEADER_TRADE_DEPOSIT_PUBLISHED.length() + "s" // lt justify
+ " %-" + COL_HEADER_TRADE_DEPOSIT_CONFIRMED.length() + "s" // lt justify
+ "%" + (COL_HEADER_TRADE_BUYER_COST.length() + 1) + "s" // rt justify
+ " %-" + (COL_HEADER_TRADE_PAYMENT_SENT.length() - 1) + "s" // left
+ " %-" + (COL_HEADER_TRADE_PAYMENT_RECEIVED.length() - 1) + "s" // left
+ " %-" + COL_HEADER_TRADE_PAYOUT_PUBLISHED.length() + "s" // lt justify
+ " %-" + (COL_HEADER_TRADE_WITHDRAWN.length() + 2) + "s";
return headerLine + formatTradeData(colDataFormat, tradeInfo, isTaker);
}
private static String formatTradeData(String format,
TradeInfo tradeInfo,
boolean isTaker) {
return String.format(format,
tradeInfo.getShortId(),
tradeInfo.getRole(),
priceFormat.apply(tradeInfo),
amountFormat.apply(tradeInfo),
makerTakerMinerTxFeeFormat.apply(tradeInfo, isTaker),
makerTakerFeeFormat.apply(tradeInfo, isTaker),
tradeInfo.getIsDepositPublished() ? YES : NO,
tradeInfo.getIsDepositUnlocked() ? YES : NO,
tradeCostFormat.apply(tradeInfo),
tradeInfo.getIsPaymentSent() ? YES : NO,
tradeInfo.getIsPaymentReceived() ? YES : NO,
tradeInfo.getIsPayoutPublished() ? YES : NO,
tradeInfo.getIsWithdrawn() ? YES : NO);
}
private static final Function<TradeInfo, String> priceHeader = (t) ->
t.getOffer().getBaseCurrencyCode().equals("XMR")
? COL_HEADER_PRICE
: COL_HEADER_PRICE_OF_ALTCOIN;
private static final Function<TradeInfo, String> priceHeaderCurrencyCode = (t) ->
t.getOffer().getBaseCurrencyCode().equals("XMR")
? t.getOffer().getCounterCurrencyCode()
: t.getOffer().getBaseCurrencyCode();
private static final BiFunction<TradeInfo, Boolean, String> makerTakerFeeHeaderCurrencyCode = (t, isTaker) -> {
return "XMR";
};
private static final Function<TradeInfo, String> paymentStatusHeaderCurrencyCode = (t) ->
t.getOffer().getBaseCurrencyCode().equals("XMR")
? t.getOffer().getCounterCurrencyCode()
: t.getOffer().getBaseCurrencyCode();
private static final Function<TradeInfo, String> priceFormat = (t) ->
t.getOffer().getBaseCurrencyCode().equals("XMR")
? formatPrice(t.getTradePrice())
: formatCryptoCurrencyPrice(t.getOffer().getPrice());
private static final Function<TradeInfo, String> amountFormat = (t) ->
t.getOffer().getBaseCurrencyCode().equals("XMR")
? formatXmr(ParsingUtils.centinerosToAtomicUnits(t.getTradeAmountAsLong()))
: formatCryptoCurrencyOfferVolume(t.getOffer().getVolume());
private static final BiFunction<TradeInfo, Boolean, String> makerTakerMinerTxFeeFormat = (t, isTaker) -> {
if (isTaker) {
return formatSatoshis(t.getTxFeeAsLong());
} else {
return formatSatoshis(t.getOffer().getTxFee());
}
};
private static final BiFunction<TradeInfo, Boolean, String> makerTakerFeeFormat = (t, isTaker) -> {
return formatXmr(ParsingUtils.centinerosToAtomicUnits(t.getTakerFeeAsLong()));
};
private static final Function<TradeInfo, String> tradeCostFormat = (t) ->
t.getOffer().getBaseCurrencyCode().equals("XMR")
? formatOfferVolume(t.getOffer().getVolume())
: formatXmr(ParsingUtils.centinerosToAtomicUnits(t.getTradeAmountAsLong()));
}

View file

@ -24,11 +24,14 @@ import joptsimple.OptionSpec;
import java.util.List; import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate;
import lombok.Getter; import lombok.Getter;
import static bisq.cli.opts.OptLabel.OPT_HELP; import static bisq.cli.opts.OptLabel.OPT_HELP;
import static java.lang.String.format;
@SuppressWarnings("unchecked")
abstract class AbstractMethodOptionParser implements MethodOpts { abstract class AbstractMethodOptionParser implements MethodOpts {
// The full command line args passed to CliMain.main(String[] args). // The full command line args passed to CliMain.main(String[] args).
@ -37,7 +40,7 @@ abstract class AbstractMethodOptionParser implements MethodOpts {
protected final OptionParser parser = new OptionParser(); protected final OptionParser parser = new OptionParser();
// The help option for a specific api method, e.g., takeoffer -help. // The help option for a specific api method, e.g., takeoffer --help.
protected final OptionSpec<Void> helpOpt = parser.accepts(OPT_HELP, "Print method help").forHelp(); protected final OptionSpec<Void> helpOpt = parser.accepts(OPT_HELP, "Print method help").forHelp();
@Getter @Getter
@ -52,7 +55,6 @@ abstract class AbstractMethodOptionParser implements MethodOpts {
public AbstractMethodOptionParser parse() { public AbstractMethodOptionParser parse() {
try { try {
options = parser.parse(new ArgumentList(args).getMethodArguments()); options = parser.parse(new ArgumentList(args).getMethodArguments());
//noinspection unchecked
nonOptionArguments = (List<String>) options.nonOptionArguments(); nonOptionArguments = (List<String>) options.nonOptionArguments();
return this; return this;
} catch (OptionException ex) { } catch (OptionException ex) {
@ -64,6 +66,17 @@ abstract class AbstractMethodOptionParser implements MethodOpts {
return options.has(helpOpt); return options.has(helpOpt);
} }
protected void verifyStringIsValidDouble(String string) {
try {
Double.valueOf(string);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(format("%s is not a number", string));
}
}
protected final Predicate<OptionSpec<String>> valueNotSpecified = (opt) ->
!options.hasArgument(opt) || options.valueOf(opt).isEmpty();
private final Function<OptionException, String> cliExceptionMessageStyle = (ex) -> { private final Function<OptionException, String> cliExceptionMessageStyle = (ex) -> {
if (ex.getMessage() == null) if (ex.getMessage() == null)
return null; return null;

View file

@ -18,14 +18,7 @@
package bisq.cli.opts; package bisq.cli.opts;
import joptsimple.OptionSpec; public class CancelOfferOptionParser extends OfferIdOptionParser implements MethodOpts {
import static bisq.cli.opts.OptLabel.OPT_OFFER_ID;
public class CancelOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to cancel")
.withRequiredArg();
public CancelOfferOptionParser(String[] args) { public CancelOfferOptionParser(String[] args) {
super(args); super(args);
@ -34,12 +27,7 @@ public class CancelOfferOptionParser extends AbstractMethodOptionParser implemen
public CancelOfferOptionParser parse() { public CancelOfferOptionParser parse() {
super.parse(); super.parse();
// Short circuit opt validation if user just wants help. // Super class will short-circuit parsing if help option is present.
if (options.has(helpOpt))
return this;
if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty())
throw new IllegalArgumentException("no offer id specified");
return this; return this;
} }

View file

@ -20,20 +20,22 @@ package bisq.cli.opts;
import joptsimple.OptionSpec; import joptsimple.OptionSpec;
import static bisq.cli.CryptoCurrencyUtil.apiDoesSupportCryptoCurrency;
import static bisq.cli.opts.OptLabel.OPT_ACCOUNT_NAME; import static bisq.cli.opts.OptLabel.OPT_ACCOUNT_NAME;
import static bisq.cli.opts.OptLabel.OPT_ADDRESS; import static bisq.cli.opts.OptLabel.OPT_ADDRESS;
import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE; import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE;
import static bisq.cli.opts.OptLabel.OPT_TRADE_INSTANT; import static bisq.cli.opts.OptLabel.OPT_TRADE_INSTANT;
import static java.lang.String.format;
public class CreateCryptoCurrencyPaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts { public class CreateCryptoCurrencyPaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> accountNameOpt = parser.accepts(OPT_ACCOUNT_NAME, "crypto currency account name") final OptionSpec<String> accountNameOpt = parser.accepts(OPT_ACCOUNT_NAME, "crypto currency account name")
.withRequiredArg(); .withRequiredArg();
final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "crypto currency code") final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "crypto currency code (xmr)")
.withRequiredArg(); .withRequiredArg();
final OptionSpec<String> addressOpt = parser.accepts(OPT_ADDRESS, "address") final OptionSpec<String> addressOpt = parser.accepts(OPT_ADDRESS, "altcoin address")
.withRequiredArg(); .withRequiredArg();
final OptionSpec<Boolean> tradeInstantOpt = parser.accepts(OPT_TRADE_INSTANT, "create trade instant account") final OptionSpec<Boolean> tradeInstantOpt = parser.accepts(OPT_TRADE_INSTANT, "create trade instant account")
@ -54,9 +56,19 @@ public class CreateCryptoCurrencyPaymentAcctOptionParser extends AbstractMethodO
if (!options.has(accountNameOpt) || options.valueOf(accountNameOpt).isEmpty()) if (!options.has(accountNameOpt) || options.valueOf(accountNameOpt).isEmpty())
throw new IllegalArgumentException("no payment account name specified"); throw new IllegalArgumentException("no payment account name specified");
if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty()) if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty())
throw new IllegalArgumentException("no currency code specified"); throw new IllegalArgumentException("no currency code specified");
String cryptoCurrencyCode = options.valueOf(currencyCodeOpt);
if (!apiDoesSupportCryptoCurrency(cryptoCurrencyCode))
throw new IllegalArgumentException(format("api does not support %s payment accounts",
cryptoCurrencyCode.toLowerCase()));
if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty())
throw new IllegalArgumentException(format("no %s address specified",
cryptoCurrencyCode.toLowerCase()));
return this; return this;
} }

View file

@ -20,22 +20,20 @@ package bisq.cli.opts;
import joptsimple.OptionSpec; import joptsimple.OptionSpec;
import java.math.BigDecimal;
import static bisq.cli.opts.OptLabel.*; import static bisq.cli.opts.OptLabel.*;
import static joptsimple.internal.Strings.EMPTY; import static joptsimple.internal.Strings.EMPTY;
public class CreateOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts { public class CreateOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT, final OptionSpec<String> paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT_ID,
"id of payment account used for offer") "id of payment account used for offer")
.withRequiredArg() .withRequiredArg()
.defaultsTo(EMPTY); .defaultsTo(EMPTY);
final OptionSpec<String> directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)") final OptionSpec<String> directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)")
.withRequiredArg(); .withRequiredArg();
final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (eur|usd|...)") final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (xmr|eur|usd|...)")
.withRequiredArg(); .withRequiredArg();
final OptionSpec<String> amountOpt = parser.accepts(OPT_AMOUNT, "amount of btc to buy or sell") final OptionSpec<String> amountOpt = parser.accepts(OPT_AMOUNT, "amount of btc to buy or sell")
@ -44,7 +42,7 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implemen
final OptionSpec<String> minAmountOpt = parser.accepts(OPT_MIN_AMOUNT, "minimum amount of btc to buy or sell") final OptionSpec<String> minAmountOpt = parser.accepts(OPT_MIN_AMOUNT, "minimum amount of btc to buy or sell")
.withOptionalArg(); .withOptionalArg();
final OptionSpec<String> mktPriceMarginOpt = parser.accepts(OPT_MKT_PRICE_MARGIN, "market btc price margin (%)") final OptionSpec<String> mktPriceMarginPctOpt = parser.accepts(OPT_MKT_PRICE_MARGIN, "market btc price margin (%)")
.withOptionalArg() .withOptionalArg()
.defaultsTo("0.00"); .defaultsTo("0.00");
@ -52,13 +50,14 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implemen
.withOptionalArg() .withOptionalArg()
.defaultsTo("0"); .defaultsTo("0");
final OptionSpec<String> securityDepositOpt = parser.accepts(OPT_SECURITY_DEPOSIT, "maker security deposit (%)") final OptionSpec<String> securityDepositPctOpt = parser.accepts(OPT_SECURITY_DEPOSIT, "maker security deposit (%)")
.withRequiredArg(); .withRequiredArg();
public CreateOfferOptionParser(String[] args) { public CreateOfferOptionParser(String[] args) {
super(args); super(args);
} }
@Override
public CreateOfferOptionParser parse() { public CreateOfferOptionParser parse() {
super.parse(); super.parse();
@ -66,9 +65,6 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implemen
if (options.has(helpOpt)) if (options.has(helpOpt))
return this; return this;
if (!options.has(paymentAccountIdOpt) || options.valueOf(paymentAccountIdOpt).isEmpty())
throw new IllegalArgumentException("no payment account id specified");
if (!options.has(directionOpt) || options.valueOf(directionOpt).isEmpty()) if (!options.has(directionOpt) || options.valueOf(directionOpt).isEmpty())
throw new IllegalArgumentException("no direction (buy|sell) specified"); throw new IllegalArgumentException("no direction (buy|sell) specified");
@ -78,17 +74,27 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implemen
if (!options.has(amountOpt) || options.valueOf(amountOpt).isEmpty()) if (!options.has(amountOpt) || options.valueOf(amountOpt).isEmpty())
throw new IllegalArgumentException("no btc amount specified"); throw new IllegalArgumentException("no btc amount specified");
if (!options.has(mktPriceMarginOpt) && !options.has(fixedPriceOpt)) if (!options.has(paymentAccountIdOpt) || options.valueOf(paymentAccountIdOpt).isEmpty())
throw new IllegalArgumentException("no payment account id specified");
if (!options.has(mktPriceMarginPctOpt) && !options.has(fixedPriceOpt))
throw new IllegalArgumentException("no market price margin or fixed price specified"); throw new IllegalArgumentException("no market price margin or fixed price specified");
if (options.has(mktPriceMarginOpt) && options.valueOf(mktPriceMarginOpt).isEmpty()) if (options.has(mktPriceMarginPctOpt)) {
throw new IllegalArgumentException("no market price margin specified"); var mktPriceMarginPctString = options.valueOf(mktPriceMarginPctOpt);
if (mktPriceMarginPctString.isEmpty())
throw new IllegalArgumentException("no market price margin specified");
else
verifyStringIsValidDouble(mktPriceMarginPctString);
}
if (options.has(fixedPriceOpt) && options.valueOf(fixedPriceOpt).isEmpty()) if (options.has(fixedPriceOpt) && options.valueOf(fixedPriceOpt).isEmpty())
throw new IllegalArgumentException("no fixed price specified"); throw new IllegalArgumentException("no fixed price specified");
if (!options.has(securityDepositOpt) || options.valueOf(securityDepositOpt).isEmpty()) if (!options.has(securityDepositPctOpt) || options.valueOf(securityDepositPctOpt).isEmpty())
throw new IllegalArgumentException("no security deposit specified"); throw new IllegalArgumentException("no security deposit specified");
else
verifyStringIsValidDouble(options.valueOf(securityDepositPctOpt));
return this; return this;
} }
@ -114,23 +120,18 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implemen
} }
public boolean isUsingMktPriceMargin() { public boolean isUsingMktPriceMargin() {
return options.has(mktPriceMarginOpt); return options.has(mktPriceMarginPctOpt);
} }
@SuppressWarnings("unused") public double getMktPriceMarginPct() {
public String getMktPriceMargin() { return isUsingMktPriceMargin() ? Double.parseDouble(options.valueOf(mktPriceMarginPctOpt)) : 0.00d;
return isUsingMktPriceMargin() ? options.valueOf(mktPriceMarginOpt) : "0.00";
}
public BigDecimal getMktPriceMarginAsBigDecimal() {
return isUsingMktPriceMargin() ? new BigDecimal(options.valueOf(mktPriceMarginOpt)) : BigDecimal.ZERO;
} }
public String getFixedPrice() { public String getFixedPrice() {
return options.has(fixedPriceOpt) ? options.valueOf(fixedPriceOpt) : "0.00"; return options.has(fixedPriceOpt) ? options.valueOf(fixedPriceOpt) : "0.00";
} }
public String getSecurityDeposit() { public double getSecurityDepositPct() {
return options.valueOf(securityDepositOpt); return Double.valueOf(options.valueOf(securityDepositPctOpt));
} }
} }

View file

@ -29,7 +29,7 @@ import static java.lang.String.format;
public class CreatePaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts { public class CreatePaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> paymentAcctFormPathOpt = parser.accepts(OPT_PAYMENT_ACCOUNT_FORM, final OptionSpec<String> paymentAcctFormPathOpt = parser.accepts(OPT_PAYMENT_ACCOUNT_FORM,
"path to json payment account form") "path to json payment account form")
.withRequiredArg(); .withRequiredArg();
public CreatePaymentAcctOptionParser(String[] args) { public CreatePaymentAcctOptionParser(String[] args) {

View file

@ -28,7 +28,7 @@ public class GetOffersOptionParser extends AbstractMethodOptionParser implements
final OptionSpec<String> directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)") final OptionSpec<String> directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)")
.withRequiredArg(); .withRequiredArg();
final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (eur|usd|...)") final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (xmr|eur|usd|...)")
.withRequiredArg(); .withRequiredArg();
public GetOffersOptionParser(String[] args) { public GetOffersOptionParser(String[] args) {

View file

@ -25,7 +25,7 @@ import static bisq.cli.opts.OptLabel.OPT_PAYMENT_METHOD_ID;
public class GetPaymentAcctFormOptionParser extends AbstractMethodOptionParser implements MethodOpts { public class GetPaymentAcctFormOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> paymentMethodIdOpt = parser.accepts(OPT_PAYMENT_METHOD_ID, final OptionSpec<String> paymentMethodIdOpt = parser.accepts(OPT_PAYMENT_METHOD_ID,
"id of payment method type used by a payment account") "id of payment method type used by a payment account")
.withRequiredArg(); .withRequiredArg();
public GetPaymentAcctFormOptionParser(String[] args) { public GetPaymentAcctFormOptionParser(String[] args) {

View file

@ -0,0 +1,84 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import bisq.proto.grpc.GetTradesRequest;
import joptsimple.OptionSpec;
import java.util.function.Predicate;
import static bisq.cli.opts.OptLabel.OPT_CATEGORY;
import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED;
import static bisq.proto.grpc.GetTradesRequest.Category.FAILED;
import static bisq.proto.grpc.GetTradesRequest.Category.OPEN;
import static java.util.Arrays.stream;
public class GetTradesOptionParser extends AbstractMethodOptionParser implements MethodOpts {
// Map valid CLI option values to gRPC request parameters.
private enum CATEGORY {
// Lower case enum fits CLI method and parameter style.
open(OPEN),
closed(CLOSED),
failed(FAILED);
private final GetTradesRequest.Category grpcRequestCategory;
CATEGORY(GetTradesRequest.Category grpcRequestCategory) {
this.grpcRequestCategory = grpcRequestCategory;
}
}
final OptionSpec<String> categoryOpt = parser.accepts(OPT_CATEGORY,
"category of trades (open|closed|failed)")
.withRequiredArg()
.defaultsTo(CATEGORY.open.name());
private final Predicate<String> isValidCategory = (c) ->
stream(CATEGORY.values()).anyMatch(v -> v.name().equalsIgnoreCase(c));
public GetTradesOptionParser(String[] args) {
super(args);
}
public GetTradesOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (options.has(categoryOpt)) {
String category = options.valueOf(categoryOpt);
if (category.isEmpty())
throw new IllegalArgumentException("no category (open|closed|failed) specified");
if (!isValidCategory.test(category))
throw new IllegalArgumentException("category must be open|closed|failed");
}
return this;
}
public GetTradesRequest.Category getCategory() {
String categoryOpt = options.valueOf(this.categoryOpt).toLowerCase();
return CATEGORY.valueOf(categoryOpt).grpcRequestCategory;
}
}

View file

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

View file

@ -24,9 +24,11 @@ public class OptLabel {
public final static String OPT_ACCOUNT_NAME = "account-name"; public final static String OPT_ACCOUNT_NAME = "account-name";
public final static String OPT_ADDRESS = "address"; public final static String OPT_ADDRESS = "address";
public final static String OPT_AMOUNT = "amount"; public final static String OPT_AMOUNT = "amount";
public final static String OPT_CATEGORY = "category";
public final static String OPT_CURRENCY_CODE = "currency-code"; public final static String OPT_CURRENCY_CODE = "currency-code";
public final static String OPT_DIRECTION = "direction"; public final static String OPT_DIRECTION = "direction";
public final static String OPT_DISPUTE_AGENT_TYPE = "dispute-agent-type"; public final static String OPT_DISPUTE_AGENT_TYPE = "dispute-agent-type";
public final static String OPT_ENABLE = "enable";
public final static String OPT_FEE_CURRENCY = "fee-currency"; public final static String OPT_FEE_CURRENCY = "fee-currency";
public final static String OPT_FIXED_PRICE = "fixed-price"; public final static String OPT_FIXED_PRICE = "fixed-price";
public final static String OPT_HELP = "help"; public final static String OPT_HELP = "help";
@ -36,7 +38,7 @@ public class OptLabel {
public final static String OPT_MIN_AMOUNT = "min-amount"; public final static String OPT_MIN_AMOUNT = "min-amount";
public final static String OPT_OFFER_ID = "offer-id"; public final static String OPT_OFFER_ID = "offer-id";
public final static String OPT_PASSWORD = "password"; public final static String OPT_PASSWORD = "password";
public final static String OPT_PAYMENT_ACCOUNT = "payment-account"; public final static String OPT_PAYMENT_ACCOUNT_ID = "payment-account-id";
public final static String OPT_PAYMENT_ACCOUNT_FORM = "payment-account-form"; public final static String OPT_PAYMENT_ACCOUNT_FORM = "payment-account-form";
public final static String OPT_PAYMENT_METHOD_ID = "payment-method-id"; public final static String OPT_PAYMENT_METHOD_ID = "payment-method-id";
public final static String OPT_PORT = "port"; public final static String OPT_PORT = "port";
@ -47,6 +49,7 @@ public class OptLabel {
public final static String OPT_TRADE_INSTANT = "trade-instant"; public final static String OPT_TRADE_INSTANT = "trade-instant";
public final static String OPT_TIMEOUT = "timeout"; public final static String OPT_TIMEOUT = "timeout";
public final static String OPT_TRANSACTION_ID = "transaction-id"; public final static String OPT_TRANSACTION_ID = "transaction-id";
public final static String OPT_TRIGGER_PRICE = "trigger-price";
public final static String OPT_TX_FEE_RATE = "tx-fee-rate"; public final static String OPT_TX_FEE_RATE = "tx-fee-rate";
public final static String OPT_WALLET_PASSWORD = "wallet-password"; public final static String OPT_WALLET_PASSWORD = "wallet-password";
public final static String OPT_NEW_WALLET_PASSWORD = "new-wallet-password"; public final static String OPT_NEW_WALLET_PASSWORD = "new-wallet-password";

View file

@ -25,7 +25,7 @@ import static bisq.cli.opts.OptLabel.OPT_TX_FEE_RATE;
public class SetTxFeeRateOptionParser extends AbstractMethodOptionParser implements MethodOpts { public class SetTxFeeRateOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> feeRateOpt = parser.accepts(OPT_TX_FEE_RATE, final OptionSpec<String> feeRateOpt = parser.accepts(OPT_TX_FEE_RATE,
"tx fee rate preference (sats/byte)") "tx fee rate preference (sats/byte)")
.withRequiredArg(); .withRequiredArg();
public SetTxFeeRateOptionParser(String[] args) { public SetTxFeeRateOptionParser(String[] args) {

View file

@ -21,30 +21,21 @@ package bisq.cli.opts;
import joptsimple.OptionSpec; import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_FEE_CURRENCY; 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_ID;
import static bisq.cli.opts.OptLabel.OPT_PAYMENT_ACCOUNT;
public class TakeOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts { public class TakeOfferOptionParser extends OfferIdOptionParser implements MethodOpts {
final OptionSpec<String> offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to take") final OptionSpec<String> paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT_ID, "id of payment account used for trade")
.withRequiredArg();
final OptionSpec<String> paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT, "id of payment account used for trade")
.withRequiredArg(); .withRequiredArg();
public TakeOfferOptionParser(String[] args) { public TakeOfferOptionParser(String[] args) {
super(args); super(args, true);
} }
public TakeOfferOptionParser parse() { public TakeOfferOptionParser parse() {
super.parse(); super.parse();
// Short circuit opt validation if user just wants help. // Super class will short-circuit parsing if help option is present.
if (options.has(helpOpt))
return this;
if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty())
throw new IllegalArgumentException("no offer id specified");
if (!options.has(paymentAccountIdOpt) || options.valueOf(paymentAccountIdOpt).isEmpty()) if (!options.has(paymentAccountIdOpt) || options.valueOf(paymentAccountIdOpt).isEmpty())
throw new IllegalArgumentException("no payment account id specified"); throw new IllegalArgumentException("no payment account id specified");
@ -52,10 +43,6 @@ public class TakeOfferOptionParser extends AbstractMethodOptionParser implements
return this; return this;
} }
public String getOfferId() {
return options.valueOf(offerIdOpt);
}
public String getPaymentAccountId() { public String getPaymentAccountId() {
return options.valueOf(paymentAccountIdOpt); return options.valueOf(paymentAccountIdOpt);
} }

View file

@ -0,0 +1,166 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.request;
import bisq.proto.grpc.CancelOfferRequest;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.GetMyOfferRequest;
import bisq.proto.grpc.GetMyOffersRequest;
import bisq.proto.grpc.GetOfferRequest;
import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.OfferInfo;
import java.util.ArrayList;
import java.util.List;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
import static protobuf.OfferDirection.BUY;
import static protobuf.OfferDirection.SELL;
import bisq.cli.GrpcStubs;
public class OffersServiceRequest {
private final GrpcStubs grpcStubs;
public OffersServiceRequest(GrpcStubs grpcStubs) {
this.grpcStubs = grpcStubs;
}
@SuppressWarnings("unused")
public OfferInfo createFixedPricedOffer(String direction,
String currencyCode,
long amount,
long minAmount,
String fixedPrice,
double securityDepositPct,
String paymentAcctId,
String makerFeeCurrencyCode) {
return createOffer(direction,
currencyCode,
amount,
minAmount,
false,
fixedPrice,
0.00,
securityDepositPct,
paymentAcctId,
"0" /* no trigger price */);
}
public OfferInfo createOffer(String direction,
String currencyCode,
long amount,
long minAmount,
boolean useMarketBasedPrice,
String fixedPrice,
double marketPriceMarginPct,
double securityDepositPct,
String paymentAcctId,
String triggerPrice) {
var request = CreateOfferRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.setAmount(amount)
.setMinAmount(minAmount)
.setUseMarketBasedPrice(useMarketBasedPrice)
.setPrice(fixedPrice)
.setMarketPriceMarginPct(marketPriceMarginPct)
.setBuyerSecurityDepositPct(securityDepositPct)
.setPaymentAccountId(paymentAcctId)
.setTriggerPrice(triggerPrice)
.build();
return grpcStubs.offersService.createOffer(request).getOffer();
}
public void cancelOffer(String offerId) {
var request = CancelOfferRequest.newBuilder()
.setId(offerId)
.build();
//noinspection ResultOfMethodCallIgnored
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 offers.isEmpty() ? offers : 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 currencyCode) {
ArrayList<OfferInfo> offers = new ArrayList<>();
offers.addAll(getMyOffers(BUY.name(), currencyCode));
offers.addAll(getMyOffers(SELL.name(), currencyCode));
return offers.isEmpty() ? offers : sortOffersByDate(offers);
}
public List<OfferInfo> getMyOffersSortedByDate(String direction, String currencyCode) {
var offers = getMyOffers(direction, currencyCode);
return offers.isEmpty() ? offers : 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(toList());
}
}

View file

@ -0,0 +1,103 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.request;
import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest;
import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import protobuf.PaymentAccount;
import protobuf.PaymentMethod;
import java.util.List;
import static java.lang.String.format;
import bisq.cli.GrpcStubs;
public class PaymentAccountsServiceRequest {
private final GrpcStubs grpcStubs;
public PaymentAccountsServiceRequest(GrpcStubs grpcStubs) {
this.grpcStubs = grpcStubs;
}
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();
}
/**
* Returns the first PaymentAccount found with the given name, or throws an
* IllegalArgumentException if not found. This method should be used with care;
* it will only return one PaymentAccount, and the account name must be an exact
* match on the name argument.
* @param accountName the name of the stored PaymentAccount to retrieve
* @return PaymentAccount with given name
*/
public PaymentAccount getPaymentAccount(String accountName) {
return getPaymentAccounts().stream()
.filter(a -> a.getAccountName().equals(accountName)).findFirst()
.orElseThrow(() ->
new IllegalArgumentException(format("payment account with name '%s' not found",
accountName)));
}
public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
String currencyCode,
String address,
boolean tradeInstant) {
var request = CreateCryptoCurrencyPaymentAccountRequest.newBuilder()
.setAccountName(accountName)
.setCurrencyCode(currencyCode)
.setAddress(address)
.setTradeInstant(tradeInstant)
.build();
return grpcStubs.paymentAccountsService.createCryptoCurrencyPaymentAccount(request).getPaymentAccount();
}
public List<PaymentMethod> getCryptoPaymentMethods() {
var request = GetCryptoCurrencyPaymentMethodsRequest.newBuilder().build();
return grpcStubs.paymentAccountsService.getCryptoCurrencyPaymentMethods(request).getPaymentMethodsList();
}
}

View file

@ -0,0 +1,110 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.request;
import bisq.proto.grpc.ConfirmPaymentReceivedRequest;
import bisq.proto.grpc.ConfirmPaymentStartedRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTradesRequest;
import bisq.proto.grpc.TakeOfferReply;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TradeInfo;
import bisq.proto.grpc.WithdrawFundsRequest;
import java.util.List;
import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED;
import static bisq.proto.grpc.GetTradesRequest.Category.FAILED;
import bisq.cli.GrpcStubs;
public class TradesServiceRequest {
private final GrpcStubs grpcStubs;
public TradesServiceRequest(GrpcStubs grpcStubs) {
this.grpcStubs = grpcStubs;
}
public TakeOfferReply getTakeOfferReply(String offerId, String paymentAccountId) {
var request = TakeOfferRequest.newBuilder()
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId)
.build();
return grpcStubs.tradesService.takeOffer(request);
}
public TradeInfo takeOffer(String offerId, String paymentAccountId) {
var reply = getTakeOfferReply(offerId, paymentAccountId);
if (reply.hasTrade())
return reply.getTrade();
else
throw new IllegalStateException(reply.getFailureReason().getDescription());
}
public TradeInfo getTrade(String tradeId) {
var request = GetTradeRequest.newBuilder()
.setTradeId(tradeId)
.build();
return grpcStubs.tradesService.getTrade(request).getTrade();
}
public List<TradeInfo> getOpenTrades() {
var request = GetTradesRequest.newBuilder()
.build();
return grpcStubs.tradesService.getTrades(request).getTradesList();
}
public List<TradeInfo> getTradeHistory(GetTradesRequest.Category category) {
if (!category.equals(CLOSED) && !category.equals(FAILED))
throw new IllegalStateException("unrecognized gettrades category parameter " + category.name());
var request = GetTradesRequest.newBuilder()
.setCategory(category)
.build();
return grpcStubs.tradesService.getTrades(request).getTradesList();
}
public void confirmPaymentStarted(String tradeId) {
var request = ConfirmPaymentStartedRequest.newBuilder()
.setTradeId(tradeId)
.build();
//noinspection ResultOfMethodCallIgnored
grpcStubs.tradesService.confirmPaymentStarted(request);
}
public void confirmPaymentReceived(String tradeId) {
var request = ConfirmPaymentReceivedRequest.newBuilder()
.setTradeId(tradeId)
.build();
//noinspection ResultOfMethodCallIgnored
grpcStubs.tradesService.confirmPaymentReceived(request);
}
public void withdrawFunds(String tradeId, String address, String memo) {
var request = WithdrawFundsRequest.newBuilder()
.setTradeId(tradeId)
.setAddress(address)
.setMemo(memo)
.build();
//noinspection ResultOfMethodCallIgnored
grpcStubs.tradesService.withdrawFunds(request);
}
}

View file

@ -0,0 +1,167 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.request;
import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.BtcBalanceInfo;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.TxFeeRateInfo;
import bisq.proto.grpc.TxInfo;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import java.util.List;
import bisq.cli.GrpcStubs;
public class WalletsServiceRequest {
private final GrpcStubs grpcStubs;
public WalletsServiceRequest(GrpcStubs grpcStubs) {
this.grpcStubs = grpcStubs;
}
public BalancesInfo getBalances() {
return getBalances("");
}
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 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 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 void lockWallet() {
var request = LockWalletRequest.newBuilder().build();
//noinspection ResultOfMethodCallIgnored
grpcStubs.walletsService.lockWallet(request);
}
public void unlockWallet(String walletPassword, long timeout) {
var request = UnlockWalletRequest.newBuilder()
.setPassword(walletPassword)
.setTimeout(timeout).build();
//noinspection ResultOfMethodCallIgnored
grpcStubs.walletsService.unlockWallet(request);
}
public void removeWalletPassword(String walletPassword) {
var request = RemoveWalletPasswordRequest.newBuilder()
.setPassword(walletPassword).build();
//noinspection ResultOfMethodCallIgnored
grpcStubs.walletsService.removeWalletPassword(request);
}
public void setWalletPassword(String walletPassword) {
var request = SetWalletPasswordRequest.newBuilder()
.setPassword(walletPassword).build();
//noinspection ResultOfMethodCallIgnored
grpcStubs.walletsService.setWalletPassword(request);
}
public void setWalletPassword(String oldWalletPassword, String newWalletPassword) {
var request = SetWalletPasswordRequest.newBuilder()
.setPassword(oldWalletPassword)
.setNewPassword(newWalletPassword).build();
//noinspection ResultOfMethodCallIgnored
grpcStubs.walletsService.setWalletPassword(request);
}
}

View file

@ -0,0 +1,155 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.stream.IntStream;
import static bisq.cli.table.column.Column.JUSTIFICATION.RIGHT;
import static com.google.common.base.Strings.padStart;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import bisq.cli.table.column.Column;
/**
* A simple table of formatted data for the CLI's output console. A table must be
* created with at least one populated column, and each column passed to the constructor
* must contain the same number of rows. Null checking is omitted because tables are
* populated by protobuf message fields which cannot be null.
*
* All data in a column has the same type: long, string, etc., but a table
* may contain an arbitrary number of columns of any type. For output formatting
* purposes, numeric and date columns should be transformed to a StringColumn type with
* formatted and justified string values before being passed to the constructor.
*
* This is not a relational, rdbms table.
*/
public class Table {
public final Column<?>[] columns;
public final int rowCount;
// Each printed column is delimited by two spaces.
private final int columnDelimiterLength = 2;
/**
* Default constructor. Takes populated Columns.
*
* @param columns containing the same number of rows
*/
public Table(Column<?>... columns) {
this.columns = columns;
this.rowCount = columns.length > 0 ? columns[0].rowCount() : 0;
validateStructure();
}
/**
* Print table data to a PrintStream.
*
* @param printStream the target output stream
*/
public void print(PrintStream printStream) {
printColumnNames(printStream);
for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) {
printRow(printStream, rowIndex);
}
}
/**
* Print table column names to a PrintStream.
*
* @param printStream the target output stream
*/
private void printColumnNames(PrintStream printStream) {
IntStream.range(0, columns.length).forEachOrdered(colIndex -> {
var c = columns[colIndex];
var justifiedName = c.getJustification().equals(RIGHT)
? padStart(c.getName(), c.getWidth(), ' ')
: c.getName();
var paddedWidth = colIndex == columns.length - 1
? c.getName().length()
: c.getWidth() + columnDelimiterLength;
printStream.printf("%-" + paddedWidth + "s", justifiedName);
});
printStream.println();
}
/**
* Print a table row to a PrintStream.
*
* @param printStream the target output stream
*/
private void printRow(PrintStream printStream, int rowIndex) {
IntStream.range(0, columns.length).forEachOrdered(colIndex -> {
var c = columns[colIndex];
var paddedWidth = colIndex == columns.length - 1
? c.getWidth()
: c.getWidth() + columnDelimiterLength;
printStream.printf("%-" + paddedWidth + "s", c.getRow(rowIndex));
if (colIndex == columns.length - 1)
printStream.println();
});
}
/**
* Returns the table's formatted output as a String.
* @return String
*/
@Override
public String toString() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (PrintStream ps = new PrintStream(baos, true, UTF_8)) {
print(ps);
}
return baos.toString();
}
/**
* Verifies the table has columns, and each column has the same number of rows.
*/
private void validateStructure() {
if (columns.length == 0)
throw new IllegalArgumentException("Table has no columns.");
if (columns[0].isEmpty())
throw new IllegalArgumentException(
format("Table's 1st column (%s) has no data.",
columns[0].getName()));
IntStream.range(1, columns.length).forEachOrdered(colIndex -> {
var c = columns[colIndex];
if (c.isEmpty())
throw new IllegalStateException(
format("Table column # %d (%s) does not have any data.",
colIndex + 1,
c.getName()));
if (this.rowCount != c.rowCount())
throw new IllegalStateException(
format("Table column # %d (%s) does not have same number of rows as 1st column.",
colIndex + 1,
c.getName()));
});
}
}

View file

@ -0,0 +1,47 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
import bisq.proto.grpc.OfferInfo;
import java.util.List;
import java.util.function.Predicate;
import bisq.cli.table.Table;
/**
* Abstract superclass for TableBuilder implementations.
*/
abstract class AbstractTableBuilder {
protected final Predicate<OfferInfo> isFiatOffer = (o) -> o.getBaseCurrencyCode().equals("BTC");
protected final TableType tableType;
protected final List<?> protos;
AbstractTableBuilder(TableType tableType, List<?> protos) {
this.tableType = tableType;
this.protos = protos;
if (protos.isEmpty())
throw new IllegalArgumentException("cannot build a table without rows");
}
public abstract Table build();
}

View file

@ -0,0 +1,255 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
import bisq.proto.grpc.ContractInfo;
import bisq.proto.grpc.TradeInfo;
import java.math.BigDecimal;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import static bisq.cli.CurrencyFormat.formatSatoshis;
import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_BUYER_DEPOSIT;
import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_SELLER_DEPOSIT;
import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL;
import static java.lang.String.format;
import static protobuf.OfferDirection.SELL;
import bisq.cli.table.column.Column;
import bisq.cli.table.column.MixedTradeFeeColumn;
abstract class AbstractTradeListBuilder extends AbstractTableBuilder {
protected final List<TradeInfo> trades;
protected final TradeTableColumnSupplier colSupplier;
protected final Column<String> colTradeId;
@Nullable
protected final Column<Long> colCreateDate;
@Nullable
protected final Column<String> colMarket;
protected final Column<String> colPrice;
@Nullable
protected final Column<String> colPriceDeviation;
@Nullable
protected final Column<String> colCurrency;
@Nullable
protected final Column<Long> colAmount;
@Nullable
protected final Column<String> colMixedAmount;
@Nullable
protected final Column<Long> colMinerTxFee;
@Nullable
protected final MixedTradeFeeColumn colMixedTradeFee;
@Nullable
protected final Column<Long> colBuyerDeposit;
@Nullable
protected final Column<Long> colSellerDeposit;
@Nullable
protected final Column<String> colPaymentMethod;
@Nullable
protected final Column<String> colRole;
@Nullable
protected final Column<String> colOfferType;
@Nullable
protected final Column<String> colClosingStatus;
// Trade detail tbl specific columns
@Nullable
protected final Column<Boolean> colIsDepositPublished;
@Nullable
protected final Column<Boolean> colIsDepositConfirmed;
@Nullable
protected final Column<Boolean> colIsPayoutPublished;
@Nullable
protected final Column<Boolean> colIsCompleted;
@Nullable
protected final Column<Long> colBisqTradeFee;
@Nullable
protected final Column<String> colTradeCost;
@Nullable
protected final Column<Boolean> colIsPaymentStartedMessageSent;
@Nullable
protected final Column<Boolean> colIsPaymentReceivedMessageSent;
@Nullable
protected final Column<String> colAltcoinReceiveAddressColumn;
AbstractTradeListBuilder(TableType tableType, List<?> protos) {
super(tableType, protos);
validate();
this.trades = protos.stream().map(p -> (TradeInfo) p).collect(Collectors.toList());
this.colSupplier = new TradeTableColumnSupplier(tableType, trades);
this.colTradeId = colSupplier.tradeIdColumn.get();
this.colCreateDate = colSupplier.createDateColumn.get();
this.colMarket = colSupplier.marketColumn.get();
this.colPrice = colSupplier.priceColumn.get();
this.colPriceDeviation = colSupplier.priceDeviationColumn.get();
this.colCurrency = colSupplier.currencyColumn.get();
this.colAmount = colSupplier.amountColumn.get();
this.colMixedAmount = colSupplier.mixedAmountColumn.get();
this.colMinerTxFee = colSupplier.minerTxFeeColumn.get();
this.colMixedTradeFee = colSupplier.mixedTradeFeeColumn.get();
this.colBuyerDeposit = colSupplier.toSecurityDepositColumn.apply(COL_HEADER_BUYER_DEPOSIT);
this.colSellerDeposit = colSupplier.toSecurityDepositColumn.apply(COL_HEADER_SELLER_DEPOSIT);
this.colPaymentMethod = colSupplier.paymentMethodColumn.get();
this.colRole = colSupplier.roleColumn.get();
this.colOfferType = colSupplier.offerTypeColumn.get();
this.colClosingStatus = colSupplier.statusDescriptionColumn.get();
// Trade detail specific columns, some in common with BSQ swap trades detail.
this.colIsDepositPublished = colSupplier.depositPublishedColumn.get();
this.colIsDepositConfirmed = colSupplier.depositConfirmedColumn.get();
this.colIsPayoutPublished = colSupplier.payoutPublishedColumn.get();
this.colIsCompleted = colSupplier.fundsWithdrawnColumn.get();
this.colBisqTradeFee = colSupplier.bisqTradeDetailFeeColumn.get();
this.colTradeCost = colSupplier.tradeCostColumn.get();
this.colIsPaymentStartedMessageSent = colSupplier.paymentStartedMessageSentColumn.get();
this.colIsPaymentReceivedMessageSent = colSupplier.paymentReceivedMessageSentColumn.get();
//noinspection ConstantConditions
this.colAltcoinReceiveAddressColumn = colSupplier.altcoinReceiveAddressColumn.get();
}
protected void validate() {
if (isTradeDetailTblBuilder.get()) {
if (protos.size() != 1)
throw new IllegalArgumentException("trade detail tbl can have only one row");
} else if (protos.isEmpty()) {
throw new IllegalArgumentException("trade tbl has no rows");
}
}
// Helper Functions
private final Supplier<Boolean> isTradeDetailTblBuilder = () -> tableType.equals(TRADE_DETAIL_TBL);
protected final Predicate<TradeInfo> isFiatTrade = (t) -> isFiatOffer.test(t.getOffer());
protected final Predicate<TradeInfo> isMyOffer = (t) -> t.getOffer().getIsMyOffer();
protected final Predicate<TradeInfo> isTaker = (t) -> t.getRole().toLowerCase().contains("taker");
protected final Predicate<TradeInfo> isSellOffer = (t) -> t.getOffer().getDirection().equals(SELL.name());
protected final Predicate<TradeInfo> isBtcSeller = (t) -> (isMyOffer.test(t) && isSellOffer.test(t))
|| (!isMyOffer.test(t) && !isSellOffer.test(t));
// Column Value Functions
// Altcoin volumes from server are string representations of decimals.
// Converting them to longs ("sats") requires shifting the decimal points
// to left: 2 for BSQ, 8 for other altcoins.
protected final Function<TradeInfo, Long> toAltcoinTradeVolumeAsLong = (t) -> new BigDecimal(t.getTradeVolume()).movePointRight(8).longValue();
protected final Function<TradeInfo, String> toTradeVolumeAsString = (t) ->
isFiatTrade.test(t)
? t.getTradeVolume()
: formatSatoshis(t.getAmountAsLong());
protected final Function<TradeInfo, Long> toTradeVolumeAsLong = (t) ->
isFiatTrade.test(t)
? Long.parseLong(t.getTradeVolume())
: toAltcoinTradeVolumeAsLong.apply(t);
protected final Function<TradeInfo, Long> toTradeAmount = (t) ->
isFiatTrade.test(t)
? t.getAmountAsLong()
: toTradeVolumeAsLong.apply(t);
protected final Function<TradeInfo, String> toMarket = (t) ->
t.getOffer().getBaseCurrencyCode() + "/"
+ t.getOffer().getCounterCurrencyCode();
protected final Function<TradeInfo, String> toPaymentCurrencyCode = (t) ->
isFiatTrade.test(t)
? t.getOffer().getCounterCurrencyCode()
: t.getOffer().getBaseCurrencyCode();
protected final Function<TradeInfo, String> toPriceDeviation = (t) ->
t.getOffer().getUseMarketBasedPrice()
? format("%.2f%s", t.getOffer().getMarketPriceMarginPct(), "%")
: "N/A";
protected final Function<TradeInfo, Long> toMyMinerTxFee = (t) -> {
return isTaker.test(t)
? t.getTxFeeAsLong()
: t.getOffer().getTxFee();
};
protected final Function<TradeInfo, Long> toTradeFeeBtc = (t) -> {
var isMyOffer = t.getOffer().getIsMyOffer();
if (isMyOffer) {
return t.getOffer().getMakerFee();
} else {
return t.getTakerFeeAsLong();
}
};
protected final Function<TradeInfo, Long> toMyMakerOrTakerFee = (t) -> {
return isTaker.test(t)
? t.getTakerFeeAsLong()
: t.getOffer().getMakerFee();
};
protected final Function<TradeInfo, String> toOfferType = (t) -> {
if (isFiatTrade.test(t)) {
return t.getOffer().getDirection() + " " + t.getOffer().getBaseCurrencyCode();
} else {
if (t.getOffer().getDirection().equals("BUY")) {
return "SELL " + t.getOffer().getBaseCurrencyCode();
} else {
return "BUY " + t.getOffer().getBaseCurrencyCode();
}
}
};
protected final Predicate<TradeInfo> showAltCoinBuyerAddress = (t) -> {
if (isFiatTrade.test(t)) {
return false;
} else {
ContractInfo contract = t.getContract();
boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker();
if (isTaker.test(t)) {
return !isBuyerMakerAndSellerTaker;
} else {
return isBuyerMakerAndSellerTaker;
}
}
};
protected final Function<TradeInfo, String> toAltcoinReceiveAddress = (t) -> {
if (showAltCoinBuyerAddress.test(t)) {
ContractInfo contract = t.getContract();
boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker();
return isBuyerMakerAndSellerTaker // (is BTC buyer / maker)
? contract.getTakerPaymentAccountPayload().getAddress()
: contract.getMakerPaymentAccountPayload().getAddress();
} else {
return "";
}
};
}

View file

@ -0,0 +1,81 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
import bisq.proto.grpc.AddressBalanceInfo;
import java.util.List;
import java.util.stream.Collectors;
import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_ADDRESS;
import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_AVAILABLE_BALANCE;
import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_CONFIRMATIONS;
import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_IS_USED_ADDRESS;
import static bisq.cli.table.builder.TableType.ADDRESS_BALANCE_TBL;
import static java.lang.String.format;
import bisq.cli.table.Table;
import bisq.cli.table.column.BooleanColumn;
import bisq.cli.table.column.Column;
import bisq.cli.table.column.LongColumn;
import bisq.cli.table.column.SatoshiColumn;
import bisq.cli.table.column.StringColumn;
/**
* Builds a {@code bisq.cli.table.Table} from a List of
* {@code bisq.proto.grpc.AddressBalanceInfo} objects.
*/
class AddressBalanceTableBuilder extends AbstractTableBuilder {
// Default columns not dynamically generated with address info.
private final Column<String> colAddress;
private final Column<Long> colAvailableBalance;
private final Column<Long> colConfirmations;
private final Column<Boolean> colIsUsed;
AddressBalanceTableBuilder(List<?> protos) {
super(ADDRESS_BALANCE_TBL, protos);
colAddress = new StringColumn(format(COL_HEADER_ADDRESS, "BTC"));
this.colAvailableBalance = new SatoshiColumn(COL_HEADER_AVAILABLE_BALANCE);
this.colConfirmations = new LongColumn(COL_HEADER_CONFIRMATIONS);
this.colIsUsed = new BooleanColumn(COL_HEADER_IS_USED_ADDRESS);
}
public Table build() {
List<AddressBalanceInfo> addresses = protos.stream()
.map(a -> (AddressBalanceInfo) a)
.collect(Collectors.toList());
// Populate columns with address info.
//noinspection SimplifyStreamApiCallChains
addresses.stream().forEachOrdered(a -> {
colAddress.addRow(a.getAddress());
colAvailableBalance.addRow(a.getBalance());
colConfirmations.addRow(a.getNumConfirmations());
colIsUsed.addRow(!a.getIsAddressUnused());
});
// Define and return the table instance with populated columns.
return new Table(colAddress,
colAvailableBalance.asStringColumn(),
colConfirmations.asStringColumn(),
colIsUsed.asStringColumn());
}
}

View file

@ -0,0 +1,73 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
import bisq.proto.grpc.BtcBalanceInfo;
import java.util.List;
import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_AVAILABLE_BALANCE;
import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_LOCKED_BALANCE;
import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_RESERVED_BALANCE;
import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_TOTAL_AVAILABLE_BALANCE;
import static bisq.cli.table.builder.TableType.BTC_BALANCE_TBL;
import bisq.cli.table.Table;
import bisq.cli.table.column.Column;
import bisq.cli.table.column.SatoshiColumn;
/**
* Builds a {@code bisq.cli.table.Table} from a
* {@code bisq.proto.grpc.BtcBalanceInfo} object.
*/
class BtcBalanceTableBuilder extends AbstractTableBuilder {
// Default columns not dynamically generated with btc balance info.
private final Column<Long> colAvailableBalance;
private final Column<Long> colReservedBalance;
private final Column<Long> colTotalAvailableBalance;
private final Column<Long> colLockedBalance;
BtcBalanceTableBuilder(List<?> protos) {
super(BTC_BALANCE_TBL, protos);
this.colAvailableBalance = new SatoshiColumn(COL_HEADER_AVAILABLE_BALANCE);
this.colReservedBalance = new SatoshiColumn(COL_HEADER_RESERVED_BALANCE);
this.colTotalAvailableBalance = new SatoshiColumn(COL_HEADER_TOTAL_AVAILABLE_BALANCE);
this.colLockedBalance = new SatoshiColumn(COL_HEADER_LOCKED_BALANCE);
}
public Table build() {
BtcBalanceInfo balance = (BtcBalanceInfo) protos.get(0);
// Populate columns with btc balance info.
colAvailableBalance.addRow(balance.getAvailableBalance());
colReservedBalance.addRow(balance.getReservedBalance());
colTotalAvailableBalance.addRow(balance.getTotalAvailableBalance());
colLockedBalance.addRow(balance.getLockedBalance());
// Define and return the table instance with populated columns.
return new Table(colAvailableBalance.asStringColumn(),
colReservedBalance.asStringColumn(),
colTotalAvailableBalance.asStringColumn(),
colLockedBalance.asStringColumn());
}
}

View file

@ -0,0 +1,73 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
import java.util.List;
import static bisq.cli.table.builder.TableType.CLOSED_TRADES_TBL;
import bisq.cli.table.Table;
@SuppressWarnings("ConstantConditions")
class ClosedTradeTableBuilder extends AbstractTradeListBuilder {
ClosedTradeTableBuilder(List<?> protos) {
super(CLOSED_TRADES_TBL, protos);
}
@Override
public Table build() {
populateColumns();
return new Table(colTradeId,
colCreateDate.asStringColumn(),
colMarket,
colPrice.justify(),
colPriceDeviation.justify(),
colAmount.asStringColumn(),
colMixedAmount.justify(),
colCurrency,
colMinerTxFee.asStringColumn(),
colMixedTradeFee.asStringColumn(),
colBuyerDeposit.asStringColumn(),
colSellerDeposit.asStringColumn(),
colOfferType,
colClosingStatus);
}
private void populateColumns() {
trades.forEach(t -> {
colTradeId.addRow(t.getTradeId());
colCreateDate.addRow(t.getDate());
colMarket.addRow(toMarket.apply(t));
colPrice.addRow(t.getPrice());
colPriceDeviation.addRow(toPriceDeviation.apply(t));
colAmount.addRow(t.getAmountAsLong());
colMixedAmount.addRow(t.getTradeVolume());
colCurrency.addRow(toPaymentCurrencyCode.apply(t));
colMinerTxFee.addRow(toMyMinerTxFee.apply(t));
colMixedTradeFee.addRow(toTradeFeeBtc.apply(t), false);
colBuyerDeposit.addRow(t.getOffer().getBuyerSecurityDeposit());
colSellerDeposit.addRow(t.getOffer().getSellerSecurityDeposit());
colOfferType.addRow(toOfferType.apply(t));
});
}
}

View file

@ -0,0 +1,66 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
import java.util.List;
import static bisq.cli.table.builder.TableType.FAILED_TRADES_TBL;
import bisq.cli.table.Table;
/**
* Builds a {@code bisq.cli.table.Table} from a list of {@code bisq.proto.grpc.TradeInfo} objects.
*/
@SuppressWarnings("ConstantConditions")
class FailedTradeTableBuilder extends AbstractTradeListBuilder {
FailedTradeTableBuilder(List<?> protos) {
super(FAILED_TRADES_TBL, protos);
}
public Table build() {
populateColumns();
return new Table(colTradeId,
colCreateDate.asStringColumn(),
colMarket,
colPrice.justify(),
colAmount.asStringColumn(),
colMixedAmount.justify(),
colCurrency,
colOfferType,
colRole,
colClosingStatus);
}
private void populateColumns() {
trades.forEach(t -> {
colTradeId.addRow(t.getTradeId());
colCreateDate.addRow(t.getDate());
colMarket.addRow(toMarket.apply(t));
colPrice.addRow(t.getPrice());
colAmount.addRow(t.getAmountAsLong());
colMixedAmount.addRow(t.getTradeVolume());
colCurrency.addRow(toPaymentCurrencyCode.apply(t));
colOfferType.addRow(toOfferType.apply(t));
colRole.addRow(t.getRole());
colClosingStatus.addRow("Failed");
});
}
}

View file

@ -0,0 +1,267 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
import bisq.proto.grpc.OfferInfo;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import static bisq.cli.table.builder.TableBuilderConstants.*;
import static bisq.cli.table.builder.TableType.OFFER_TBL;
import static bisq.cli.table.column.Column.JUSTIFICATION.LEFT;
import static bisq.cli.table.column.Column.JUSTIFICATION.NONE;
import static bisq.cli.table.column.Column.JUSTIFICATION.RIGHT;
import static bisq.cli.table.column.ZippedStringColumns.DUPLICATION_MODE.EXCLUDE_DUPLICATES;
import static java.lang.String.format;
import static protobuf.OfferDirection.BUY;
import static protobuf.OfferDirection.SELL;
import bisq.cli.table.Table;
import bisq.cli.table.column.Column;
import bisq.cli.table.column.Iso8601DateTimeColumn;
import bisq.cli.table.column.SatoshiColumn;
import bisq.cli.table.column.StringColumn;
import bisq.cli.table.column.ZippedStringColumns;
/**
* Builds a {@code bisq.cli.table.Table} from a List of
* {@code bisq.proto.grpc.OfferInfo} objects.
*/
class OfferTableBuilder extends AbstractTableBuilder {
// Columns common to both fiat and cryptocurrency offers.
private final Column<String> colOfferId = new StringColumn(COL_HEADER_UUID, LEFT);
private final Column<String> colDirection = new StringColumn(COL_HEADER_DIRECTION, LEFT);
private final Column<Long> colAmount = new SatoshiColumn("Temp Amount", NONE);
private final Column<Long> colMinAmount = new SatoshiColumn("Temp Min Amount", NONE);
private final Column<String> colPaymentMethod = new StringColumn(COL_HEADER_PAYMENT_METHOD, LEFT);
private final Column<Long> colCreateDate = new Iso8601DateTimeColumn(COL_HEADER_CREATION_DATE);
OfferTableBuilder(List<?> protos) {
super(OFFER_TBL, protos);
}
@Override
public Table build() {
List<OfferInfo> offers = protos.stream().map(p -> (OfferInfo) p).collect(Collectors.toList());
return isShowingFiatOffers.get()
? buildFiatOfferTable(offers)
: buildCryptoCurrencyOfferTable(offers);
}
@SuppressWarnings("ConstantConditions")
public Table buildFiatOfferTable(List<OfferInfo> offers) {
@Nullable
Column<String> colEnabled = enabledColumn.get(); // Not boolean: "YES", "NO", or "PENDING"
Column<String> colFiatPrice = new StringColumn(format(COL_HEADER_DETAILED_PRICE, fiatTradeCurrency.get()), RIGHT);
Column<String> colVolume = new StringColumn(format("Temp Volume (%s)", fiatTradeCurrency.get()), NONE);
Column<String> colMinVolume = new StringColumn(format("Temp Min Volume (%s)", fiatTradeCurrency.get()), NONE);
@Nullable
Column<String> colTriggerPrice = fiatTriggerPriceColumn.get();
// Populate columns with offer info.
offers.forEach(o -> {
if (colEnabled != null)
colEnabled.addRow(toEnabled.apply(o));
colDirection.addRow(o.getDirection());
colFiatPrice.addRow(o.getPrice());
colMinAmount.addRow(o.getMinAmount());
colAmount.addRow(o.getAmount());
colVolume.addRow(o.getVolume());
colMinVolume.addRow(o.getMinVolume());
if (colTriggerPrice != null)
colTriggerPrice.addRow(toBlankOrNonZeroValue.apply(o.getTriggerPrice()));
colPaymentMethod.addRow(o.getPaymentMethodShortName());
colCreateDate.addRow(o.getDate());
colOfferId.addRow(o.getId());
});
ZippedStringColumns amountRange = zippedAmountRangeColumns.get();
ZippedStringColumns volumeRange =
new ZippedStringColumns(format(COL_HEADER_VOLUME_RANGE, fiatTradeCurrency.get()),
RIGHT,
" - ",
colMinVolume.asStringColumn(),
colVolume.asStringColumn());
// Define and return the table instance with populated columns.
if (isShowingMyOffers.get()) {
return new Table(colEnabled.asStringColumn(),
colDirection,
colFiatPrice.justify(),
amountRange.asStringColumn(EXCLUDE_DUPLICATES),
volumeRange.asStringColumn(EXCLUDE_DUPLICATES),
colTriggerPrice.justify(),
colPaymentMethod,
colCreateDate.asStringColumn(),
colOfferId);
} else {
return new Table(colDirection,
colFiatPrice.justify(),
amountRange.asStringColumn(EXCLUDE_DUPLICATES),
volumeRange.asStringColumn(EXCLUDE_DUPLICATES),
colPaymentMethod,
colCreateDate.asStringColumn(),
colOfferId);
}
}
@SuppressWarnings("ConstantConditions")
public Table buildCryptoCurrencyOfferTable(List<OfferInfo> offers) {
@Nullable
Column<String> colEnabled = enabledColumn.get(); // Not boolean: YES, NO, or PENDING
Column<String> colBtcPrice = new StringColumn(format(COL_HEADER_DETAILED_PRICE_OF_ALTCOIN, altcoinTradeCurrency.get()), RIGHT);
Column<String> colVolume = new StringColumn(format("Temp Volume (%s)", altcoinTradeCurrency.get()), NONE);
Column<String> colMinVolume = new StringColumn(format("Temp Min Volume (%s)", altcoinTradeCurrency.get()), NONE);
@Nullable
Column<String> colTriggerPrice = altcoinTriggerPriceColumn.get();
// Populate columns with offer info.
offers.forEach(o -> {
if (colEnabled != null)
colEnabled.addRow(toEnabled.apply(o));
colDirection.addRow(directionFormat.apply(o));
colBtcPrice.addRow(o.getPrice());
colAmount.addRow(o.getAmount());
colMinAmount.addRow(o.getMinAmount());
colVolume.addRow(o.getVolume());
colMinVolume.addRow(o.getMinVolume());
if (colTriggerPrice != null)
colTriggerPrice.addRow(toBlankOrNonZeroValue.apply(o.getTriggerPrice()));
colPaymentMethod.addRow(o.getPaymentMethodShortName());
colCreateDate.addRow(o.getDate());
colOfferId.addRow(o.getId());
});
ZippedStringColumns amountRange = zippedAmountRangeColumns.get();
ZippedStringColumns volumeRange =
new ZippedStringColumns(format(COL_HEADER_VOLUME_RANGE, altcoinTradeCurrency.get()),
RIGHT,
" - ",
colMinVolume.asStringColumn(),
colVolume.asStringColumn());
// Define and return the table instance with populated columns.
if (isShowingMyOffers.get()) {
if (isShowingBsqOffers.get()) {
return new Table(colEnabled.asStringColumn(),
colDirection,
colBtcPrice.justify(),
amountRange.asStringColumn(EXCLUDE_DUPLICATES),
volumeRange.asStringColumn(EXCLUDE_DUPLICATES),
colPaymentMethod,
colCreateDate.asStringColumn(),
colOfferId);
} else {
return new Table(colEnabled.asStringColumn(),
colDirection,
colBtcPrice.justify(),
amountRange.asStringColumn(EXCLUDE_DUPLICATES),
volumeRange.asStringColumn(EXCLUDE_DUPLICATES),
colTriggerPrice.justify(),
colPaymentMethod,
colCreateDate.asStringColumn(),
colOfferId);
}
} else {
return new Table(colDirection,
colBtcPrice.justify(),
amountRange.asStringColumn(EXCLUDE_DUPLICATES),
volumeRange.asStringColumn(EXCLUDE_DUPLICATES),
colPaymentMethod,
colCreateDate.asStringColumn(),
colOfferId);
}
}
private final Function<String, String> toBlankOrNonZeroValue = (s) -> s.trim().equals("0") ? "" : s;
private final Supplier<OfferInfo> firstOfferInList = () -> (OfferInfo) protos.get(0);
private final Supplier<Boolean> isShowingMyOffers = () -> firstOfferInList.get().getIsMyOffer();
private final Supplier<Boolean> isShowingFiatOffers = () -> isFiatOffer.test(firstOfferInList.get());
private final Supplier<String> fiatTradeCurrency = () -> firstOfferInList.get().getCounterCurrencyCode();
private final Supplier<String> altcoinTradeCurrency = () -> firstOfferInList.get().getBaseCurrencyCode();
private final Supplier<Boolean> isShowingBsqOffers = () ->
!isFiatOffer.test(firstOfferInList.get()) && altcoinTradeCurrency.get().equals("BSQ");
@Nullable // Not a boolean column: YES, NO, or PENDING.
private final Supplier<StringColumn> enabledColumn = () ->
isShowingMyOffers.get()
? new StringColumn(COL_HEADER_ENABLED, LEFT)
: null;
@Nullable
private final Supplier<StringColumn> fiatTriggerPriceColumn = () ->
isShowingMyOffers.get()
? new StringColumn(format(COL_HEADER_TRIGGER_PRICE, fiatTradeCurrency.get()), RIGHT)
: null;
@Nullable
private final Supplier<StringColumn> altcoinTriggerPriceColumn = () ->
isShowingMyOffers.get() && !isShowingBsqOffers.get()
? new StringColumn(format(COL_HEADER_TRIGGER_PRICE, altcoinTradeCurrency.get()), RIGHT)
: null;
private final Function<OfferInfo, String> toEnabled = (o) -> {
return o.getIsActivated() ? "YES" : "NO";
};
private final Function<String, String> toMirroredDirection = (d) ->
d.equalsIgnoreCase(BUY.name()) ? SELL.name() : BUY.name();
private final Function<OfferInfo, String> directionFormat = (o) -> {
if (isFiatOffer.test(o)) {
return o.getBaseCurrencyCode();
} else {
// Return "Sell BSQ (Buy BTC)", or "Buy BSQ (Sell BTC)".
String direction = o.getDirection();
String mirroredDirection = toMirroredDirection.apply(direction);
Function<String, String> mixedCase = (word) -> word.charAt(0) + word.substring(1).toLowerCase();
return format("%s %s (%s %s)",
mixedCase.apply(mirroredDirection),
o.getBaseCurrencyCode(),
mixedCase.apply(direction),
o.getCounterCurrencyCode());
}
};
private final Supplier<ZippedStringColumns> zippedAmountRangeColumns = () -> {
if (colMinAmount.isEmpty() || colAmount.isEmpty())
throw new IllegalStateException("amount columns must have data");
return new ZippedStringColumns(COL_HEADER_AMOUNT_RANGE,
RIGHT,
" - ",
colMinAmount.asStringColumn(),
colAmount.asStringColumn());
};
}

View file

@ -0,0 +1,64 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
import java.util.List;
import static bisq.cli.table.builder.TableType.OPEN_TRADES_TBL;
import bisq.cli.table.Table;
/**
* Builds a {@code bisq.cli.table.Table} from a list of {@code bisq.proto.grpc.TradeInfo} objects.
*/
@SuppressWarnings("ConstantConditions")
class OpenTradeTableBuilder extends AbstractTradeListBuilder {
OpenTradeTableBuilder(List<?> protos) {
super(OPEN_TRADES_TBL, protos);
}
public Table build() {
populateColumns();
return new Table(colTradeId,
colCreateDate.asStringColumn(),
colMarket,
colPrice.justify(),
colAmount.asStringColumn(),
colMixedAmount.justify(),
colCurrency,
colPaymentMethod,
colRole);
}
private void populateColumns() {
trades.forEach(t -> {
colTradeId.addRow(t.getTradeId());
colCreateDate.addRow(t.getDate());
colMarket.addRow(toMarket.apply(t));
colPrice.addRow(t.getPrice());
colAmount.addRow(t.getAmountAsLong());
colMixedAmount.addRow(t.getTradeVolume());
colCurrency.addRow(toPaymentCurrencyCode.apply(t));
colPaymentMethod.addRow(t.getOffer().getPaymentMethodShortName());
colRole.addRow(t.getRole());
});
}
}

View file

@ -0,0 +1,74 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
import protobuf.PaymentAccount;
import java.util.List;
import java.util.stream.Collectors;
import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_CURRENCY;
import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_NAME;
import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_PAYMENT_METHOD;
import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_UUID;
import static bisq.cli.table.builder.TableType.PAYMENT_ACCOUNT_TBL;
import bisq.cli.table.Table;
import bisq.cli.table.column.Column;
import bisq.cli.table.column.StringColumn;
/**
* Builds a {@code bisq.cli.table.Table} from a List of
* {@code protobuf.PaymentAccount} objects.
*/
class PaymentAccountTableBuilder extends AbstractTableBuilder {
// Default columns not dynamically generated with payment account info.
private final Column<String> colName;
private final Column<String> colCurrency;
private final Column<String> colPaymentMethod;
private final Column<String> colId;
PaymentAccountTableBuilder(List<?> protos) {
super(PAYMENT_ACCOUNT_TBL, protos);
this.colName = new StringColumn(COL_HEADER_NAME);
this.colCurrency = new StringColumn(COL_HEADER_CURRENCY);
this.colPaymentMethod = new StringColumn(COL_HEADER_PAYMENT_METHOD);
this.colId = new StringColumn(COL_HEADER_UUID);
}
public Table build() {
List<PaymentAccount> paymentAccounts = protos.stream()
.map(a -> (PaymentAccount) a)
.collect(Collectors.toList());
// Populate columns with payment account info.
//noinspection SimplifyStreamApiCallChains
paymentAccounts.stream().forEachOrdered(a -> {
colName.addRow(a.getAccountName());
colCurrency.addRow(a.getSelectedTradeCurrency().getCode());
colPaymentMethod.addRow(a.getPaymentMethod().getId());
colId.addRow(a.getId());
});
// Define and return the table instance with populated columns.
return new Table(colName, colCurrency, colPaymentMethod, colId);
}
}

View file

@ -0,0 +1,69 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
import java.util.List;
import static java.util.Collections.singletonList;
import bisq.cli.table.Table;
/**
* Table builder factory. It is not conventionally named TableBuilderFactory because
* it has no static factory methods. The number of static fields and methods in the
* {@code bisq.cli.table} are kept to a minimum in an effort o reduce class load time
* in the session-less CLI.
*/
public class TableBuilder extends AbstractTableBuilder {
public TableBuilder(TableType tableType, Object proto) {
this(tableType, singletonList(proto));
}
public TableBuilder(TableType tableType, List<?> protos) {
super(tableType, protos);
}
@Override
public Table build() {
switch (tableType) {
case ADDRESS_BALANCE_TBL:
return new AddressBalanceTableBuilder(protos).build();
case BTC_BALANCE_TBL:
return new BtcBalanceTableBuilder(protos).build();
case CLOSED_TRADES_TBL:
return new ClosedTradeTableBuilder(protos).build();
case FAILED_TRADES_TBL:
return new FailedTradeTableBuilder(protos).build();
case OFFER_TBL:
return new OfferTableBuilder(protos).build();
case OPEN_TRADES_TBL:
return new OpenTradeTableBuilder(protos).build();
case PAYMENT_ACCOUNT_TBL:
return new PaymentAccountTableBuilder(protos).build();
case TRADE_DETAIL_TBL:
return new TradeDetailTableBuilder(protos).build();
case TRANSACTION_TBL:
return new TransactionTableBuilder(protos).build();
default:
throw new IllegalArgumentException("invalid cli table type " + tableType.name());
}
}
}

View file

@ -0,0 +1,82 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
/**
* Table column name constants.
*/
class TableBuilderConstants {
static final String COL_HEADER_ADDRESS = "%-3s Address";
static final String COL_HEADER_AMOUNT = "Amount";
static final String COL_HEADER_AMOUNT_IN_BTC = "Amount in BTC";
static final String COL_HEADER_AMOUNT_RANGE = "BTC(min - max)";
static final String COL_HEADER_AVAILABLE_BALANCE = "Available Balance";
static final String COL_HEADER_AVAILABLE_CONFIRMED_BALANCE = "Available Confirmed Balance";
static final String COL_HEADER_UNCONFIRMED_CHANGE_BALANCE = "Unconfirmed Change Balance";
static final String COL_HEADER_RESERVED_BALANCE = "Reserved Balance";
static final String COL_HEADER_TOTAL_AVAILABLE_BALANCE = "Total Available Balance";
static final String COL_HEADER_LOCKED_BALANCE = "Locked Balance";
static final String COL_HEADER_LOCKED_FOR_VOTING_BALANCE = "Locked For Voting Balance";
static final String COL_HEADER_LOCKUP_BONDS_BALANCE = "Lockup Bonds Balance";
static final String COL_HEADER_UNLOCKING_BONDS_BALANCE = "Unlocking Bonds Balance";
static final String COL_HEADER_UNVERIFIED_BALANCE = "Unverified Balance";
static final String COL_HEADER_BSQ_SWAP_TRADE_ROLE = "My BSQ Swap Role";
static final String COL_HEADER_BUYER_DEPOSIT = "Buyer Deposit (BTC)";
static final String COL_HEADER_SELLER_DEPOSIT = "Seller Deposit (BTC)";
static final String COL_HEADER_CONFIRMATIONS = "Confirmations";
static final String COL_HEADER_DEVIATION = "Deviation";
static final String COL_HEADER_IS_USED_ADDRESS = "Is Used";
static final String COL_HEADER_CREATION_DATE = "Creation Date (UTC)";
static final String COL_HEADER_CURRENCY = "Currency";
static final String COL_HEADER_DATE_TIME = "Date/Time (UTC)";
static final String COL_HEADER_DETAILED_AMOUNT = "Amount(%-3s)";
static final String COL_HEADER_DETAILED_PRICE = "Price in %-3s for 1 BTC";
static final String COL_HEADER_DETAILED_PRICE_OF_ALTCOIN = "Price in BTC for 1 %-3s";
static final String COL_HEADER_DIRECTION = "Buy/Sell";
static final String COL_HEADER_ENABLED = "Enabled";
static final String COL_HEADER_MARKET = "Market";
static final String COL_HEADER_NAME = "Name";
static final String COL_HEADER_OFFER_TYPE = "Offer Type";
static final String COL_HEADER_PAYMENT_METHOD = "Payment Method";
static final String COL_HEADER_PRICE = "Price";
static final String COL_HEADER_STATUS = "Status";
static final String COL_HEADER_TRADE_ALTCOIN_BUYER_ADDRESS = "%-3s Buyer Address";
static final String COL_HEADER_TRADE_BUYER_COST = "Buyer Cost(%-3s)";
static final String COL_HEADER_TRADE_DEPOSIT_CONFIRMED = "Deposit Confirmed";
static final String COL_HEADER_TRADE_DEPOSIT_PUBLISHED = "Deposit Published";
static final String COL_HEADER_TRADE_PAYMENT_SENT = "%-3s Sent";
static final String COL_HEADER_TRADE_PAYMENT_RECEIVED = "%-3s Received";
static final String COL_HEADER_TRADE_PAYOUT_PUBLISHED = "Payout Published";
static final String COL_HEADER_TRADE_WITHDRAWN = "Withdrawn";
static final String COL_HEADER_TRADE_ID = "Trade ID";
static final String COL_HEADER_TRADE_ROLE = "My Role";
static final String COL_HEADER_TRADE_SHORT_ID = "ID";
static final String COL_HEADER_TRADE_MAKER_FEE = "Maker Fee(%-3s)";
static final String COL_HEADER_TRADE_TAKER_FEE = "Taker Fee(%-3s)";
static final String COL_HEADER_TRADE_FEE = "Trade Fee";
static final String COL_HEADER_TRIGGER_PRICE = "Trigger Price(%-3s)";
static final String COL_HEADER_TX_ID = "Tx ID";
static final String COL_HEADER_TX_INPUT_SUM = "Tx Inputs (BTC)";
static final String COL_HEADER_TX_OUTPUT_SUM = "Tx Outputs (BTC)";
static final String COL_HEADER_TX_FEE = "Tx Fee (BTC)";
static final String COL_HEADER_TX_SIZE = "Tx Size (Bytes)";
static final String COL_HEADER_TX_IS_CONFIRMED = "Is Confirmed";
static final String COL_HEADER_TX_MEMO = "Memo";
static final String COL_HEADER_VOLUME_RANGE = "%-3s(min - max)";
static final String COL_HEADER_UUID = "ID";
}

View file

@ -0,0 +1,34 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
/**
* Used as param in TableBuilder constructor instead of inspecting
* protos to find out what kind of CLI output table should be built.
*/
public enum TableType {
ADDRESS_BALANCE_TBL,
BTC_BALANCE_TBL,
CLOSED_TRADES_TBL,
FAILED_TRADES_TBL,
OFFER_TBL,
OPEN_TRADES_TBL,
PAYMENT_ACCOUNT_TBL,
TRADE_DETAIL_TBL,
TRANSACTION_TBL
}

View file

@ -0,0 +1,105 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
import bisq.proto.grpc.TradeInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL;
import static java.lang.String.format;
import bisq.cli.table.Table;
import bisq.cli.table.column.Column;
/**
* Builds a {@code bisq.cli.table.Table} from a {@code bisq.proto.grpc.TradeInfo} object.
*/
@SuppressWarnings("ConstantConditions")
class TradeDetailTableBuilder extends AbstractTradeListBuilder {
TradeDetailTableBuilder(List<?> protos) {
super(TRADE_DETAIL_TBL, protos);
}
/**
* Build a single row trade detail table.
* @return Table containing one row
*/
@Override
public Table build() {
// A trade detail table only has one row.
var trade = trades.get(0);
populateColumns(trade);
List<Column<?>> columns = defineColumnList(trade);
return new Table(columns.toArray(new Column<?>[0]));
}
private void populateColumns(TradeInfo trade) {
populateBisqV1TradeColumns(trade);
}
private void populateBisqV1TradeColumns(TradeInfo trade) {
colTradeId.addRow(trade.getShortId());
colRole.addRow(trade.getRole());
colPrice.addRow(trade.getPrice());
colAmount.addRow(toTradeAmount.apply(trade));
colMinerTxFee.addRow(toMyMinerTxFee.apply(trade));
colBisqTradeFee.addRow(toMyMakerOrTakerFee.apply(trade));
colIsDepositPublished.addRow(trade.getIsDepositPublished());
colIsDepositConfirmed.addRow(trade.getIsDepositUnlocked());
colTradeCost.addRow(toTradeVolumeAsString.apply(trade));
colIsPaymentStartedMessageSent.addRow(trade.getIsPaymentSent());
colIsPaymentReceivedMessageSent.addRow(trade.getIsPaymentReceived());
colIsPayoutPublished.addRow(trade.getIsPayoutPublished());
colIsCompleted.addRow(trade.getIsCompleted());
if (colAltcoinReceiveAddressColumn != null)
colAltcoinReceiveAddressColumn.addRow(toAltcoinReceiveAddress.apply(trade));
}
private List<Column<?>> defineColumnList(TradeInfo trade) {
return getBisqV1TradeColumnList();
}
private List<Column<?>> getBisqV1TradeColumnList() {
List<Column<?>> columns = new ArrayList<>() {{
add(colTradeId);
add(colRole);
add(colPrice.justify());
add(colAmount.asStringColumn());
add(colMinerTxFee.asStringColumn());
add(colBisqTradeFee.asStringColumn());
add(colIsDepositPublished.asStringColumn());
add(colIsDepositConfirmed.asStringColumn());
add(colTradeCost.justify());
add(colIsPaymentStartedMessageSent.asStringColumn());
add(colIsPaymentReceivedMessageSent.asStringColumn());
add(colIsPayoutPublished.asStringColumn());
add(colIsCompleted.asStringColumn());
}};
if (colAltcoinReceiveAddressColumn != null)
columns.add(colAltcoinReceiveAddressColumn);
return columns;
}
}

View file

@ -0,0 +1,267 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
import bisq.proto.grpc.ContractInfo;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.TradeInfo;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static bisq.cli.table.builder.TableBuilderConstants.*;
import static bisq.cli.table.builder.TableType.CLOSED_TRADES_TBL;
import static bisq.cli.table.builder.TableType.FAILED_TRADES_TBL;
import static bisq.cli.table.builder.TableType.OPEN_TRADES_TBL;
import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL;
import static bisq.cli.table.column.AltcoinVolumeColumn.DISPLAY_MODE.ALTCOIN_VOLUME;
import static bisq.cli.table.column.AltcoinVolumeColumn.DISPLAY_MODE.BSQ_VOLUME;
import static bisq.cli.table.column.Column.JUSTIFICATION.LEFT;
import static bisq.cli.table.column.Column.JUSTIFICATION.RIGHT;
import static java.lang.String.format;
import bisq.cli.table.column.AltcoinVolumeColumn;
import bisq.cli.table.column.BooleanColumn;
import bisq.cli.table.column.BtcColumn;
import bisq.cli.table.column.Column;
import bisq.cli.table.column.Iso8601DateTimeColumn;
import bisq.cli.table.column.MixedTradeFeeColumn;
import bisq.cli.table.column.SatoshiColumn;
import bisq.cli.table.column.StringColumn;
/**
* Convenience for supplying column definitions to
* open/closed/failed/detail trade table builders.
*/
@Slf4j
class TradeTableColumnSupplier {
@Getter
private final TableType tableType;
@Getter
private final List<TradeInfo> trades;
public TradeTableColumnSupplier(TableType tableType, List<TradeInfo> trades) {
this.tableType = tableType;
this.trades = trades;
}
private final Supplier<Boolean> isTradeDetailTblBuilder = () -> getTableType().equals(TRADE_DETAIL_TBL);
private final Supplier<Boolean> isOpenTradeTblBuilder = () -> getTableType().equals(OPEN_TRADES_TBL);
private final Supplier<Boolean> isClosedTradeTblBuilder = () -> getTableType().equals(CLOSED_TRADES_TBL);
private final Supplier<Boolean> isFailedTradeTblBuilder = () -> getTableType().equals(FAILED_TRADES_TBL);
private final Supplier<TradeInfo> firstRow = () -> getTrades().get(0);
private final Predicate<OfferInfo> isFiatOffer = (o) -> o.getBaseCurrencyCode().equals("BTC");
private final Predicate<TradeInfo> isFiatTrade = (t) -> isFiatOffer.test(t.getOffer());
private final Predicate<TradeInfo> isTaker = (t) -> t.getRole().toLowerCase().contains("taker");
final Supplier<StringColumn> tradeIdColumn = () -> isTradeDetailTblBuilder.get()
? new StringColumn(COL_HEADER_TRADE_SHORT_ID)
: new StringColumn(COL_HEADER_TRADE_ID);
final Supplier<Iso8601DateTimeColumn> createDateColumn = () -> isTradeDetailTblBuilder.get()
? null
: new Iso8601DateTimeColumn(COL_HEADER_DATE_TIME);
final Supplier<StringColumn> marketColumn = () -> isTradeDetailTblBuilder.get()
? null
: new StringColumn(COL_HEADER_MARKET);
private final Function<TradeInfo, Column<String>> toDetailedPriceColumn = (t) -> {
String colHeader = isFiatTrade.test(t)
? format(COL_HEADER_DETAILED_PRICE, t.getOffer().getCounterCurrencyCode())
: format(COL_HEADER_DETAILED_PRICE_OF_ALTCOIN, t.getOffer().getBaseCurrencyCode());
return new StringColumn(colHeader, RIGHT);
};
final Supplier<Column<String>> priceColumn = () -> isTradeDetailTblBuilder.get()
? toDetailedPriceColumn.apply(firstRow.get())
: new StringColumn(COL_HEADER_PRICE, RIGHT);
final Supplier<Column<String>> priceDeviationColumn = () -> isTradeDetailTblBuilder.get()
? null
: new StringColumn(COL_HEADER_DEVIATION, RIGHT);
final Supplier<StringColumn> currencyColumn = () -> isTradeDetailTblBuilder.get()
? null
: new StringColumn(COL_HEADER_CURRENCY);
private final Function<TradeInfo, Column<Long>> toDetailedAmountColumn = (t) -> {
String headerCurrencyCode = t.getOffer().getBaseCurrencyCode();
String colHeader = format(COL_HEADER_DETAILED_AMOUNT, headerCurrencyCode);
AltcoinVolumeColumn.DISPLAY_MODE displayMode = headerCurrencyCode.equals("BSQ") ? BSQ_VOLUME : ALTCOIN_VOLUME;
return isFiatTrade.test(t)
? new SatoshiColumn(colHeader)
: new AltcoinVolumeColumn(colHeader, displayMode);
};
// Can be fiat, btc or altcoin amount represented as longs. Placing the decimal
// in the displayed string representation is done in the Column implementation.
final Supplier<Column<Long>> amountColumn = () -> isTradeDetailTblBuilder.get()
? toDetailedAmountColumn.apply(firstRow.get())
: new BtcColumn(COL_HEADER_AMOUNT_IN_BTC);
final Supplier<StringColumn> mixedAmountColumn = () -> isTradeDetailTblBuilder.get()
? null
: new StringColumn(COL_HEADER_AMOUNT, RIGHT);
final Supplier<Column<Long>> minerTxFeeColumn = () -> isTradeDetailTblBuilder.get() || isClosedTradeTblBuilder.get()
? new SatoshiColumn(COL_HEADER_TX_FEE)
: null;
final Supplier<MixedTradeFeeColumn> mixedTradeFeeColumn = () -> isTradeDetailTblBuilder.get()
? null
: new MixedTradeFeeColumn(COL_HEADER_TRADE_FEE);
final Supplier<StringColumn> paymentMethodColumn = () -> isTradeDetailTblBuilder.get() || isClosedTradeTblBuilder.get()
? null
: new StringColumn(COL_HEADER_PAYMENT_METHOD, LEFT);
final Supplier<StringColumn> roleColumn = () -> {
return isTradeDetailTblBuilder.get() || isOpenTradeTblBuilder.get() || isFailedTradeTblBuilder.get()
? new StringColumn(COL_HEADER_TRADE_ROLE)
: null;
};
final Function<String, Column<Long>> toSecurityDepositColumn = (name) -> isClosedTradeTblBuilder.get()
? new SatoshiColumn(name)
: null;
final Supplier<StringColumn> offerTypeColumn = () -> isTradeDetailTblBuilder.get()
? null
: new StringColumn(COL_HEADER_OFFER_TYPE);
final Supplier<StringColumn> statusDescriptionColumn = () -> isTradeDetailTblBuilder.get()
? null
: new StringColumn(COL_HEADER_STATUS);
private final Function<String, Column<Boolean>> toBooleanColumn = BooleanColumn::new;
final Supplier<Column<Boolean>> depositPublishedColumn = () -> {
return isTradeDetailTblBuilder.get()
? toBooleanColumn.apply(COL_HEADER_TRADE_DEPOSIT_PUBLISHED)
: null;
};
final Supplier<Column<Boolean>> depositConfirmedColumn = () -> {
return isTradeDetailTblBuilder.get()
? toBooleanColumn.apply(COL_HEADER_TRADE_DEPOSIT_CONFIRMED)
: null;
};
final Supplier<Column<Boolean>> payoutPublishedColumn = () -> {
return isTradeDetailTblBuilder.get()
? toBooleanColumn.apply(COL_HEADER_TRADE_PAYOUT_PUBLISHED)
: null;
};
final Supplier<Column<Boolean>> fundsWithdrawnColumn = () -> {
return isTradeDetailTblBuilder.get()
? toBooleanColumn.apply(COL_HEADER_TRADE_WITHDRAWN)
: null;
};
final Supplier<Column<Long>> bisqTradeDetailFeeColumn = () -> {
if (isTradeDetailTblBuilder.get()) {
TradeInfo t = firstRow.get();
String headerCurrencyCode = "XMR";
String colHeader = isTaker.test(t)
? format(COL_HEADER_TRADE_TAKER_FEE, headerCurrencyCode)
: format(COL_HEADER_TRADE_MAKER_FEE, headerCurrencyCode);
return new SatoshiColumn(colHeader, false);
} else {
return null;
}
};
final Function<TradeInfo, String> toPaymentCurrencyCode = (t) ->
isFiatTrade.test(t)
? t.getOffer().getCounterCurrencyCode()
: t.getOffer().getBaseCurrencyCode();
final Supplier<Column<Boolean>> paymentStartedMessageSentColumn = () -> {
if (isTradeDetailTblBuilder.get()) {
String headerCurrencyCode = toPaymentCurrencyCode.apply(firstRow.get());
String colHeader = format(COL_HEADER_TRADE_PAYMENT_SENT, headerCurrencyCode);
return new BooleanColumn(colHeader);
} else {
return null;
}
};
final Supplier<Column<Boolean>> paymentReceivedMessageSentColumn = () -> {
if (isTradeDetailTblBuilder.get()) {
String headerCurrencyCode = toPaymentCurrencyCode.apply(firstRow.get());
String colHeader = format(COL_HEADER_TRADE_PAYMENT_RECEIVED, headerCurrencyCode);
return new BooleanColumn(colHeader);
} else {
return null;
}
};
final Supplier<Column<String>> tradeCostColumn = () -> {
if (isTradeDetailTblBuilder.get()) {
TradeInfo t = firstRow.get();
String headerCurrencyCode = t.getOffer().getCounterCurrencyCode();
String colHeader = format(COL_HEADER_TRADE_BUYER_COST, headerCurrencyCode);
return new StringColumn(colHeader, RIGHT);
} else {
return null;
}
};
final Predicate<TradeInfo> showAltCoinBuyerAddress = (t) -> {
if (isFiatTrade.test(t)) {
return false;
} else {
ContractInfo contract = t.getContract();
boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker();
if (isTaker.test(t)) {
return !isBuyerMakerAndSellerTaker;
} else {
return isBuyerMakerAndSellerTaker;
}
}
};
@Nullable
final Supplier<Column<String>> altcoinReceiveAddressColumn = () -> {
if (isTradeDetailTblBuilder.get()) {
TradeInfo t = firstRow.get();
if (showAltCoinBuyerAddress.test(t)) {
String headerCurrencyCode = toPaymentCurrencyCode.apply(t);
String colHeader = format(COL_HEADER_TRADE_ALTCOIN_BUYER_ADDRESS, headerCurrencyCode);
return new StringColumn(colHeader);
} else {
return null;
}
} else {
return null;
}
};
}

View file

@ -0,0 +1,103 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
import bisq.proto.grpc.TxInfo;
import java.util.List;
import javax.annotation.Nullable;
import static bisq.cli.table.builder.TableBuilderConstants.*;
import static bisq.cli.table.builder.TableType.TRANSACTION_TBL;
import bisq.cli.table.Table;
import bisq.cli.table.column.BooleanColumn;
import bisq.cli.table.column.Column;
import bisq.cli.table.column.LongColumn;
import bisq.cli.table.column.SatoshiColumn;
import bisq.cli.table.column.StringColumn;
/**
* Builds a {@code bisq.cli.table.Table} from a {@code bisq.proto.grpc.TxInfo} object.
*/
class TransactionTableBuilder extends AbstractTableBuilder {
// Default columns not dynamically generated with tx info.
private final Column<String> colTxId;
private final Column<Boolean> colIsConfirmed;
private final Column<Long> colInputSum;
private final Column<Long> colOutputSum;
private final Column<Long> colTxFee;
private final Column<Long> colTxSize;
TransactionTableBuilder(List<?> protos) {
super(TRANSACTION_TBL, protos);
this.colTxId = new StringColumn(COL_HEADER_TX_ID);
this.colIsConfirmed = new BooleanColumn(COL_HEADER_TX_IS_CONFIRMED);
this.colInputSum = new SatoshiColumn(COL_HEADER_TX_INPUT_SUM);
this.colOutputSum = new SatoshiColumn(COL_HEADER_TX_OUTPUT_SUM);
this.colTxFee = new SatoshiColumn(COL_HEADER_TX_FEE);
this.colTxSize = new LongColumn(COL_HEADER_TX_SIZE);
}
public Table build() {
// TODO Add 'gettransactions' api method & show multiple tx in the console.
// For now, a tx tbl is only one row.
TxInfo tx = (TxInfo) protos.get(0);
// Declare the columns derived from tx info.
@Nullable
Column<String> colMemo = tx.getMemo().isEmpty()
? null
: new StringColumn(COL_HEADER_TX_MEMO);
// Populate columns with tx info.
colTxId.addRow(tx.getTxId());
colIsConfirmed.addRow(!tx.getIsPending());
colInputSum.addRow(tx.getInputSum());
colOutputSum.addRow(tx.getOutputSum());
colTxFee.addRow(tx.getFee());
colTxSize.addRow((long) tx.getSize());
if (colMemo != null)
colMemo.addRow(tx.getMemo());
// Define and return the table instance with populated columns.
if (colMemo != null) {
return new Table(colTxId,
colIsConfirmed.asStringColumn(),
colInputSum.asStringColumn(),
colOutputSum.asStringColumn(),
colTxFee.asStringColumn(),
colTxSize.asStringColumn(),
colMemo);
} else {
return new Table(colTxId,
colIsConfirmed.asStringColumn(),
colInputSum.asStringColumn(),
colOutputSum.asStringColumn(),
colTxFee.asStringColumn(),
colTxSize.asStringColumn());
}
}
}

View file

@ -0,0 +1,86 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.column;
import static bisq.cli.table.column.Column.JUSTIFICATION.RIGHT;
import static com.google.common.base.Strings.padEnd;
import static com.google.common.base.Strings.padStart;
/**
* Partial implementation of the {@link Column} interface.
*/
abstract class AbstractColumn<C extends Column<T>, T> implements Column<T> {
// We create an encapsulated StringColumn up front to populate with formatted
// strings in each this.addRow(Long value) call. But we will not know how
// to justify the cached, formatted string until the column is fully populated.
protected final StringColumn stringColumn;
// The name field is not final, so it can be re-set for column alignment.
protected String name;
protected final JUSTIFICATION justification;
// The max width is not known until after column is fully populated.
protected int maxWidth;
public AbstractColumn(String name, JUSTIFICATION justification) {
this.name = name;
this.justification = justification;
this.stringColumn = this instanceof StringColumn ? null : new StringColumn(name, justification);
}
@Override
public String getName() {
return this.name;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public int getWidth() {
return maxWidth;
}
@Override
public JUSTIFICATION getJustification() {
return this.justification;
}
@Override
public Column<T> justify() {
if (this instanceof StringColumn && this.justification.equals(RIGHT))
return this.justify();
else
return this; // no-op
}
protected final String toJustifiedString(String s) {
switch (justification) {
case LEFT:
return padEnd(s, maxWidth, ' ');
case RIGHT:
return padStart(s, maxWidth, ' ');
case NONE:
default:
return s;
}
}
}

View file

@ -0,0 +1,89 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.column;
import java.math.BigDecimal;
import java.util.function.BiFunction;
import java.util.stream.IntStream;
import static bisq.cli.table.column.Column.JUSTIFICATION.RIGHT;
/**
* For displaying altcoin volume with appropriate precision.
*/
public class AltcoinVolumeColumn extends LongColumn {
public enum DISPLAY_MODE {
ALTCOIN_VOLUME,
BSQ_VOLUME,
}
private final DISPLAY_MODE displayMode;
// The default AltcoinVolumeColumn JUSTIFICATION is RIGHT.
public AltcoinVolumeColumn(String name, DISPLAY_MODE displayMode) {
this(name, RIGHT, displayMode);
}
public AltcoinVolumeColumn(String name,
JUSTIFICATION justification,
DISPLAY_MODE displayMode) {
super(name, justification);
this.displayMode = displayMode;
}
@Override
public void addRow(Long value) {
rows.add(value);
String s = toFormattedString.apply(value, displayMode);
stringColumn.addRow(s);
if (isNewMaxWidth.test(s))
maxWidth = s.length();
}
@Override
public String getRowAsFormattedString(int rowIndex) {
return toFormattedString.apply(getRow(rowIndex), displayMode);
}
@Override
public StringColumn asStringColumn() {
// We cached the formatted altcoin value strings, but we did
// not know how much padding each string needed until now.
IntStream.range(0, stringColumn.getRows().size()).forEach(rowIndex -> {
String unjustified = stringColumn.getRow(rowIndex);
String justified = stringColumn.toJustifiedString(unjustified);
stringColumn.updateRow(rowIndex, justified);
});
return this.stringColumn;
}
private final BiFunction<Long, DISPLAY_MODE, String> toFormattedString = (value, displayMode) -> {
switch (displayMode) {
case ALTCOIN_VOLUME:
return value > 0 ? new BigDecimal(value).movePointLeft(8).toString() : "";
case BSQ_VOLUME:
return value > 0 ? new BigDecimal(value).movePointLeft(2).toString() : "";
default:
throw new IllegalStateException("invalid display mode: " + displayMode);
}
};
}

View file

@ -0,0 +1,131 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.column;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.IntStream;
import static bisq.cli.table.column.Column.JUSTIFICATION.LEFT;
/**
* For displaying boolean values as YES, NO, or user's choice for 'true' and 'false'.
*/
public class BooleanColumn extends AbstractColumn<BooleanColumn, Boolean> {
private static final String DEFAULT_TRUE_AS_STRING = "YES";
private static final String DEFAULT_FALSE_AS_STRING = "NO";
private final List<Boolean> rows = new ArrayList<>();
private final Predicate<String> isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth;
private final String trueAsString;
private final String falseAsString;
// The default BooleanColumn JUSTIFICATION is LEFT.
// The default BooleanColumn True AsString value is YES.
// The default BooleanColumn False AsString value is NO.
public BooleanColumn(String name) {
this(name, LEFT, DEFAULT_TRUE_AS_STRING, DEFAULT_FALSE_AS_STRING);
}
// Use this constructor to override default LEFT justification.
@SuppressWarnings("unused")
public BooleanColumn(String name, JUSTIFICATION justification) {
this(name, justification, DEFAULT_TRUE_AS_STRING, DEFAULT_FALSE_AS_STRING);
}
// Use this constructor to override default true/false as string defaults.
public BooleanColumn(String name, String trueAsString, String falseAsString) {
this(name, LEFT, trueAsString, falseAsString);
}
// Use this constructor to override default LEFT justification.
public BooleanColumn(String name,
JUSTIFICATION justification,
String trueAsString,
String falseAsString) {
super(name, justification);
this.trueAsString = trueAsString;
this.falseAsString = falseAsString;
this.maxWidth = name.length();
}
@Override
public void addRow(Boolean value) {
rows.add(value);
// We do not know how much padding each StringColumn value needs until it has all the values.
String s = asString(value);
stringColumn.addRow(s);
if (isNewMaxWidth.test(s))
maxWidth = s.length();
}
@Override
public List<Boolean> getRows() {
return rows;
}
@Override
public int rowCount() {
return rows.size();
}
@Override
public boolean isEmpty() {
return rows.isEmpty();
}
@Override
public Boolean getRow(int rowIndex) {
return rows.get(rowIndex);
}
@Override
public void updateRow(int rowIndex, Boolean newValue) {
rows.set(rowIndex, newValue);
}
@Override
public String getRowAsFormattedString(int rowIndex) {
return getRow(rowIndex)
? trueAsString
: falseAsString;
}
@Override
public StringColumn asStringColumn() {
// We cached the formatted satoshi strings, but we did
// not know how much padding each string needed until now.
IntStream.range(0, stringColumn.getRows().size()).forEach(rowIndex -> {
String unjustified = stringColumn.getRow(rowIndex);
String justified = stringColumn.toJustifiedString(unjustified);
stringColumn.updateRow(rowIndex, justified);
});
return stringColumn;
}
private String asString(boolean value) {
return value ? trueAsString : falseAsString;
}
}

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