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
**Bitcoin-Core**: Version 0.19, 0.20, or 0.21
**Bitcoin-Core**: Version 0.19 - 22
**Git Client**
@ -252,9 +252,9 @@ To remove a custom withdrawal transaction fee rate preference, and revert to the
$ ./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
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
```
### 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
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
The `trade-simulation.sh` script described above is an easy way to figure out how to use this command.
In a previous example, Alice created a BUY/ EUR offer to buy 0.125 BTC at a fixed price of 30,800 EUR,
and pay the Bisq maker fee in BTC. Alice had already created an EUR face-to-face payment account with id
In a previous example, Alice created a BUY/ EUR offer to buy 0.125 BTC at a fixed price of 30,800 EUR.
Alice had already created an EUR face-to-face payment account with id
`f3c1ec8b-9761-458d-b13d-9039c6892413`, and used this `createoffer` command:
```
$ ./bisq-cli --password=xyz --port=9998 createoffer \
--payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \
--payment-account-id=f3c1ec8b-9761-458d-b13d-9039c6892413 \
--direction=BUY \
--currency-code=EUR \
--amount=0.125 \
--fixed-price=30800 \
--security-deposit=15.0 \
--fee-currency=BTC
--security-deposit=15.0
```
If Alice was in Japan, and wanted to create an offer to sell 0.125 BTC at 0.5% above the current market JPY price,
putting up a 15% security deposit, the `createoffer` command to do that would be:
```
$ ./bisq-cli --password=xyz --port=9998 createoffer \
--payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \
--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 \
--fee-currency=BTC
--security-deposit=15.0
```
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:
```
$ ./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
@ -365,8 +378,116 @@ The offer will be removed from other Bisq users' offer views, and paid transacti
### Editing an Existing Offer
Editing existing offers is not yet supported. You can cancel and re-create an offer, but paid transaction fees
for the canceled offer will be forfeited.
Offers you create can be edited in various ways:
- 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
@ -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
```
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:
```
$ ./bisq-cli --password=xyz --port=9998 takeoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--payment-account=fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e \
--fee-currency=btc
--payment-account-id=fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e
```
The taken offer will be used to create a trade contract. The next section describes how to use the Api to execute
the trade.
The next section describes how to use the Api to execute a trade.
### 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.
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
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
because they inform the Bisq node that a trades state can be set to `CLOSED`. Please close out your trades with one
The last two mutually exclusive commands (`closetrade` or `withdrawfunds`) may seem unnecessary, but they are critical
because they tell the Bisq node to set a completed trades state `CLOSED`. Please close out your trades with one
or the other command.
Each of the CLI commands above takes one argument: `--trade-id=<trade-id>`:
```
$ ./bisq-cli --password=xyz --port=9998 confirmpaymentstarted --trade-id=<trade-id>
$ ./bisq-cli --password=xyz --port=9999 confirmpaymentreceived --trade-id=<trade-id>
$ ./bisq-cli --password=xyz --port=9998 keepfunds --trade-id=<trade-id>
$ ./bisq-cli --password=xyz --port=9998 closetrade --trade-id=<trade-id>
$ ./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
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:
$ ./gradlew clean build

View File

@ -6,7 +6,7 @@
#
# 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.
# Build command: `./gradlew clean build :apitest:installDaoSetup`
@ -134,7 +134,7 @@ sleeptraced 3
# Show Alice's new offer.
printdate "ALICE: Looking at her new $DIRECTION $CURRENCY_CODE offer."
CMD="$CLI_BASE --port=$ALICE_PORT getmyoffer --offer-id=$OFFER_ID"
CMD="$CLI_BASE --port=$ALICE_PORT getoffer --offer-id=$OFFER_ID"
printdate "ALICE CLI: $CMD"
OFFER=$($CMD)
exitoncommandalert $?

View File

@ -10,7 +10,7 @@
#
# 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.
# Build command: `./gradlew clean build :apitest:installDaoSetup`
@ -94,7 +94,7 @@ while : ; do
# Show Alice's new offer.
printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer."
CMD="$CLI_BASE --port=$ALICE_PORT getmyoffer --offer-id=$OFFER_ID"
CMD="$CLI_BASE --port=$ALICE_PORT getoffer --offer-id=$OFFER_ID"
printdate "ALICE CLI: $CMD"
OFFER=$($CMD)
exitoncommandalert $?

View File

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

View File

@ -1,13 +1,13 @@
#! /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
# trade, and the maker's face to face payment account's currency code is used when creating the offer.
#
# Prerequisites:
#
# - Linux or OSX with bash, Java 10, or Java 11-12 (JDK language compatibility 10), and bitcoin-core (v0.19, v0.20, or v0.21).
# - 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`
@ -26,15 +26,16 @@
#
# `$ 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:
#
# 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`
#
# 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:
#
# `$ 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
printbreak
registerdisputeagents
# Demonstrate how to create a country based, face to face account.
showcreatepaymentacctsteps "Alice" "$ALICE_PORT"
@ -96,7 +95,7 @@ sleeptraced 3
# Show Alice's new offer.
printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer."
CMD="$CLI_BASE --port=$ALICE_PORT getmyoffer --offer-id=$OFFER_ID"
CMD="$CLI_BASE --port=$ALICE_PORT getoffer --offer-id=$OFFER_ID"
printdate "ALICE CLI: $CMD"
OFFER=$($CMD)
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;
import java.io.File;
import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.Scaffold.EXIT_FAILURE;
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.exit;
@ -32,7 +37,7 @@ import bisq.apitest.config.ApiTestConfig;
* ApiTestMain is a placeholder for the gradle build file, which requires a valid
* 'mainClassName' property in the :apitest subproject configuration.
*
* It does has some uses:
* It has some uses:
*
* 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:
* 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
public class ApiTestMain {
public static void main(String[] 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 {
log.info("Configuring test harness with options:\n\t{}", String.join("\n\t", args));
Scaffold scaffold = new Scaffold(args).setUp();
ApiTestConfig config = scaffold.config;
@ -77,4 +86,9 @@ public class ApiTestMain {
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 {
log.info("Shutting down executor service ...");
executor.shutdownNow();
//noinspection ResultOfMethodCallIgnored
executor.awaitTermination(config.supportingApps.size() * 2000L, MILLISECONDS);
SetupTask[] orderedTasks = new SetupTask[]{
@ -189,7 +190,7 @@ public class Scaffold {
MILLISECONDS.sleep(1000);
if (p.hasShutdownExceptions()) {
// We log shutdown exceptions, but do not throw any from here
// because all of the background instances must be shut down.
// because all the background instances must be shut down.
p.logExceptions(p.getShutdownExceptions(), log);
// 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 {
if (config.callRateMeteringConfigPath.isEmpty())
return;
File testRateMeteringFile = new File(config.callRateMeteringConfigPath);
if (!testRateMeteringFile.exists())
throw new FileNotFoundException(
@ -289,49 +293,49 @@ public class Scaffold {
startBisqApp(bobdesktop, executor, countdownLatch);
}
private void startBisqApp(HavenoAppConfig havenoAppConfig,
private void startBisqApp(HavenoAppConfig HavenoAppConfig,
ExecutorService executor,
CountDownLatch countdownLatch)
throws IOException, InterruptedException {
HavenoProcess bisqProcess = createBisqProcess(havenoAppConfig);
switch (havenoAppConfig) {
HavenoProcess HavenoProcess = createHavenoProcess(HavenoAppConfig);
switch (HavenoAppConfig) {
case seednode:
seedNodeTask = new SetupTask(bisqProcess, countdownLatch);
seedNodeTask = new SetupTask(HavenoProcess, countdownLatch);
seedNodeTaskFuture = executor.submit(seedNodeTask);
break;
case arbdaemon:
case arbdesktop:
arbNodeTask = new SetupTask(bisqProcess, countdownLatch);
arbNodeTask = new SetupTask(HavenoProcess, countdownLatch);
arbNodeTaskFuture = executor.submit(arbNodeTask);
break;
case alicedaemon:
case alicedesktop:
aliceNodeTask = new SetupTask(bisqProcess, countdownLatch);
aliceNodeTask = new SetupTask(HavenoProcess, countdownLatch);
aliceNodeTaskFuture = executor.submit(aliceNodeTask);
break;
case bobdaemon:
case bobdesktop:
bobNodeTask = new SetupTask(bisqProcess, countdownLatch);
bobNodeTask = new SetupTask(HavenoProcess, countdownLatch);
bobNodeTaskFuture = executor.submit(bobNodeTask);
break;
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);
if (bisqProcess.hasStartupExceptions()) {
bisqProcess.logExceptions(bisqProcess.getStartupExceptions(), log);
throw new IllegalStateException(bisqProcess.getStartupExceptions().get(0));
if (HavenoProcess.hasStartupExceptions()) {
HavenoProcess.logExceptions(HavenoProcess.getStartupExceptions(), log);
throw new IllegalStateException(HavenoProcess.getStartupExceptions().get(0));
}
}
private HavenoProcess createBisqProcess(HavenoAppConfig havenoAppConfig)
private HavenoProcess createHavenoProcess(HavenoAppConfig HavenoAppConfig)
throws IOException, InterruptedException {
HavenoProcess bisqProcess = new HavenoProcess(havenoAppConfig, config);
bisqProcess.verifyAppNotRunning();
bisqProcess.verifyAppDataDirInstalled();
return bisqProcess;
HavenoProcess HavenoProcess = new HavenoProcess(HavenoAppConfig, config);
HavenoProcess.verifyAppNotRunning();
HavenoProcess.verifyAppDataDirInstalled();
return HavenoProcess;
}
private void verifyStartupCompleted()

View File

@ -56,6 +56,8 @@ public class ApiTestConfig {
// Global constants
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 ARBITRATOR = "arbitrator";
public static final String MEDIATOR = "mediator";

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();
if (!bitcoindExecutable.exists() || !bitcoindExecutable.canExecute())
throw new IllegalStateException(format("'%s' cannot be found or executed.%n"
+ "A bitcoin-core v0.19, v0.20, or v0.21 installation is required," +
+ "A bitcoin-core v0.19 - v22 installation is required," +
" and the 'bitcoinPath' must be configured in 'apitest.properties'",
bitcoindExecutable.getAbsolutePath()));

View File

@ -24,7 +24,7 @@ import java.util.List;
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 java.lang.management.ManagementFactory.getRuntimeMXBean;
@ -33,7 +33,9 @@ import static java.lang.management.ManagementFactory.getRuntimeMXBean;
public class BashCommand {
private int exitStatus = -1;
@Nullable
private String output;
@Nullable
private String error;
private final String command;
@ -92,6 +94,7 @@ public class BashCommand {
}
// TODO return Optional<String>
@Nullable
public String getOutput() {
return this.output;
}
@ -101,7 +104,6 @@ public class BashCommand {
return this.error;
}
@NotNull
private List<String> tokenizeSystemCommand() {
return new ArrayList<>() {{
add(BASH_PATH_VALUE);

View File

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

View File

@ -19,17 +19,35 @@ package bisq.apitest.method;
import bisq.core.api.model.PaymentAccountForm;
import bisq.core.payment.F2FAccount;
import bisq.core.payment.NationalBankAccount;
import bisq.core.proto.CoreProtoResolver;
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.IOException;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.util.function.Function;
import java.util.function.Supplier;
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.util.Arrays.stream;
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.linux.BashCommand;
import bisq.cli.GrpcClient;
import bisq.cli.table.builder.TableBuilder;
public class MethodTest extends ApiTestCase {
@ -46,15 +66,6 @@ public class MethodTest extends ApiTestCase {
private static final Function<Enum<?>[], String> toNameList = (enums) ->
stream(enums).map(Enum::name).collect(Collectors.joining(","));
public static void startSupportingApps(File callRateMeteringConfigFile,
boolean generateBtcBlock,
Enum<?>... supportingApps) {
startSupportingApps(callRateMeteringConfigFile,
generateBtcBlock,
false,
supportingApps);
}
public static void startSupportingApps(File callRateMeteringConfigFile,
boolean generateBtcBlock,
boolean startSupportingAppsInDebugMode,
@ -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,
boolean startSupportingAppsInDebugMode,
Enum<?>... supportingApps) {
try {
// Disable call rate metering where there is no callRateMeteringConfigFile.
File callRateMeteringConfigFile = defaultRateMeterInterceptorConfig();
File callRateMeteringConfigFile = getTestRateMeterInterceptorConfig();
setUpScaffold(new String[]{
"--supportingApps", toNameList.apply(supportingApps),
"--callRateMeteringConfigPath", callRateMeteringConfigFile.getAbsolutePath(),
@ -133,17 +137,94 @@ public class MethodTest extends ApiTestCase {
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) {
// Normally, we do asserts on the protos from the gRPC service, but in this
// case we need a bisq.core.payment.PaymentAccount so it can be cast to its
// sub type.
// sub-type.
var paymentAccount = grpcClient.createPaymentAccount(jsonString);
return bisq.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER);
}
// Static conveniences for test methods and test case fixture setups.
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) {
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() {
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
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());
}

View File

@ -17,13 +17,15 @@
package bisq.apitest.method.offer;
import bisq.core.monetary.Altcoin;
import bisq.proto.grpc.OfferInfo;
import protobuf.PaymentAccount;
import org.bitcoinj.utils.Fiat;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.List;
import java.util.function.Function;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ -32,49 +34,96 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.ApiTestConfig.XMR;
import static bisq.apitest.config.HavenoAppConfig.alicedaemon;
import static bisq.apitest.config.HavenoAppConfig.arbdaemon;
import static bisq.apitest.config.HavenoAppConfig.bobdaemon;
import static bisq.apitest.config.HavenoAppConfig.seednode;
import static bisq.common.util.MathUtils.roundDouble;
import static bisq.common.util.MathUtils.scaleDownByPowerOf10;
import static bisq.core.locale.CurrencyUtil.isCryptoCurrency;
import static java.math.RoundingMode.HALF_UP;
import static bisq.cli.table.builder.TableType.OFFER_TBL;
import static java.lang.String.format;
import static java.lang.System.out;
import bisq.apitest.method.MethodTest;
import bisq.cli.CliMain;
import bisq.cli.table.builder.TableBuilder;
@Slf4j
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
protected static boolean isLongRunningTest;
protected static PaymentAccount alicesBtcAcct;
protected static PaymentAccount bobsBtcAcct;
protected static PaymentAccount alicesXmrAcct;
protected static PaymentAccount bobsXmrAcct;
@BeforeAll
public static void setUp() {
setUp(false);
}
public static void setUp(boolean startSupportingAppsInDebugMode) {
startSupportingApps(true,
false,
startSupportingAppsInDebugMode,
bitcoind,
seednode,
arbdaemon,
alicedaemon,
bobdaemon);
initPaymentAccounts();
}
protected double getScaledOfferPrice(double offerPrice, String currencyCode) {
int precision = isCryptoCurrency(currencyCode) ? Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT;
return scaleDownByPowerOf10(offerPrice, precision);
protected static final Function<OfferInfo, String> toOfferTable = (offer) ->
new TableBuilder(OFFER_TBL, offer).build().toString();
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) {
return BigDecimal.valueOf(roundDouble((1 - (price1 / price2)), 5))
.setScale(4, HALF_UP)
.doubleValue();
@SuppressWarnings("ConstantConditions")
public static void initPaymentAccounts() {
alicesBtcAcct = aliceClient.getPaymentAccount("BTC");
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
public static void tearDown() {
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.TestMethodOrder;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OfferDirection.BUY;
@Disabled
@Slf4j
@ -51,8 +50,9 @@ public class CancelOfferTest extends AbstractOfferTest {
10000000L,
10000000L,
0.00,
getDefaultBuyerSecurityDepositAsPercent(),
paymentAccountId);
defaultBuyerSecurityDepositPct.get(),
paymentAccountId,
NO_TRIGGER_PRICE);
};
@Test

View File

@ -28,14 +28,14 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.XMR;
import static bisq.cli.TableFormat.formatOfferTable;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static java.util.Collections.singletonList;
import static bisq.apitest.config.ApiTestConfig.EUR;
import static bisq.apitest.config.ApiTestConfig.USD;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OfferPayload.Direction.SELL;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static protobuf.OfferDirection.BUY;
import static protobuf.OfferDirection.SELL;
@Disabled
@Slf4j
@ -44,35 +44,44 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
@Test
@Order(1)
public void testCreateAUDXMRBuyOfferUsingFixedPrice16000() {
public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() {
PaymentAccount audAccount = createDummyF2FAccount(aliceClient, "AU");
var newOffer = aliceClient.createFixedPricedOffer(BUY.name(),
"aud",
10_000_000L,
10_000_000L,
"36000",
getDefaultBuyerSecurityDepositAsPercent(),
defaultBuyerSecurityDepositPct.get(),
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();
assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection());
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.getMinAmount());
assertEquals("3600", newOffer.getVolume());
assertEquals("3600", newOffer.getMinVolume());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(audAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals("AUD", newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getMyOffer(newOfferId);
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection());
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.getMinAmount());
assertEquals("3600", newOffer.getVolume());
assertEquals("3600", newOffer.getMinVolume());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(audAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
@ -81,75 +90,93 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
@Test
@Order(2)
public void testCreateUSDXMRBuyOfferUsingFixedPrice100001234() {
public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() {
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
var newOffer = aliceClient.createFixedPricedOffer(BUY.name(),
"usd",
10_000_000L,
10_000_000L,
"30000.1234",
getDefaultBuyerSecurityDepositAsPercent(),
defaultBuyerSecurityDepositPct.get(),
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();
assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection());
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.getMinAmount());
assertEquals("3000", newOffer.getVolume());
assertEquals("3000", newOffer.getMinVolume());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId());
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(BUY.name(), newOffer.getDirection());
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.getMinAmount());
assertEquals("3000", newOffer.getVolume());
assertEquals("3000", newOffer.getMinVolume());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals("USD", newOffer.getCounterCurrencyCode());
assertEquals(USD, newOffer.getCounterCurrencyCode());
}
@Test
@Order(3)
public void testCreateEURXMRSellOfferUsingFixedPrice95001234() {
public void testCreateEURBTCSellOfferUsingFixedPrice95001234() {
PaymentAccount eurAccount = createDummyF2FAccount(aliceClient, "FR");
var newOffer = aliceClient.createFixedPricedOffer(SELL.name(),
"eur",
10_000_000L,
5_000_000L,
"29500.1234",
getDefaultBuyerSecurityDepositAsPercent(),
defaultBuyerSecurityDepositPct.get(),
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();
assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals(295_001_234, newOffer.getPrice());
assertEquals("29500.1234", newOffer.getPrice());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals("2950", newOffer.getVolume());
assertEquals("1475", newOffer.getMinVolume());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(eurAccount.getId(), newOffer.getPaymentAccountId());
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(SELL.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals(295_001_234, newOffer.getPrice());
assertEquals("29500.1234", newOffer.getPrice());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals("2950", newOffer.getVolume());
assertEquals("1475", newOffer.getMinVolume());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(eurAccount.getId(), newOffer.getPaymentAccountId());
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.math.BigDecimal;
import lombok.extern.slf4j.Slf4j;
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.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.XMR;
import static bisq.cli.TableFormat.formatOfferTable;
import static bisq.apitest.config.ApiTestConfig.BTC;
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.scaleUpByPowerOf10;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static java.lang.Math.abs;
import static java.lang.String.format;
import static java.util.Collections.singletonList;
import static java.math.RoundingMode.HALF_UP;
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.OfferPayload.Direction.BUY;
import static protobuf.OfferPayload.Direction.SELL;
import static protobuf.OfferDirection.BUY;
import static protobuf.OfferDirection.SELL;
@SuppressWarnings("ConstantConditions")
@Disabled
@Slf4j
@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_WARNING_TOLERANCE = 0.0001; // 0.01%
private static final String MAKER_FEE_CURRENCY_CODE = BTC;
@Test
@Order(1)
public void testCreateUSDXMRBuyOffer5PctPriceMargin() {
public void testCreateUSDBTCBuyOffer5PctPriceMargin() {
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
double priceMarginPctInput = 5.00;
double priceMarginPctInput = 5.00d;
var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
"usd",
10_000_000L,
10_000_000L,
priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(),
usdAccount.getId());
log.info("OFFER #1:\n{}", formatOfferTable(singletonList(newOffer), "usd"));
defaultBuyerSecurityDepositPct.get(),
usdAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals("USD", newOffer.getCounterCurrencyCode());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals(USD, newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getMyOffer(newOfferId);
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals("USD", newOffer.getCounterCurrencyCode());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals(USD, newOffer.getCounterCurrencyCode());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
}
@Test
@Order(2)
public void testCreateNZDXMRBuyOfferMinus2PctPriceMargin() {
public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() {
PaymentAccount nzdAccount = createDummyF2FAccount(aliceClient, "NZ");
double priceMarginPctInput = -2.00;
double priceMarginPctInput = -2.00d; // -2%
var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
"nzd",
10_000_000L,
10_000_000L,
priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(),
nzdAccount.getId());
log.info("OFFER #2:\n{}", formatOfferTable(singletonList(newOffer), "nzd"));
defaultBuyerSecurityDepositPct.get(),
nzdAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(nzdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("NZD", newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getMyOffer(newOfferId);
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(nzdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("NZD", newOffer.getCounterCurrencyCode());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
@ -132,7 +154,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
@Test
@Order(3)
public void testCreateGBPXMRSellOfferMinus1Point5PctPriceMargin() {
public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() {
PaymentAccount gbpAccount = createDummyF2FAccount(aliceClient, "GB");
double priceMarginPctInput = -1.5;
var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(),
@ -140,29 +162,37 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L,
5_000_000L,
priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(),
gbpAccount.getId());
log.info("OFFER #3:\n{}", formatOfferTable(singletonList(newOffer), "gbp"));
defaultBuyerSecurityDepositPct.get(),
gbpAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #3:\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(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(gbpAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("GBP", newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getMyOffer(newOfferId);
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(gbpAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("GBP", newOffer.getCounterCurrencyCode());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
@ -170,7 +200,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
@Test
@Order(4)
public void testCreateBRLXMRSellOffer6Point55PctPriceMargin() {
public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() {
PaymentAccount brlAccount = createDummyF2FAccount(aliceClient, "BR");
double priceMarginPctInput = 6.55;
var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(),
@ -178,53 +208,92 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L,
5_000_000L,
priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(),
brlAccount.getId());
log.info("OFFER #4:\n{}", formatOfferTable(singletonList(newOffer), "brl"));
defaultBuyerSecurityDepositPct.get(),
brlAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #4:\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(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(brlAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("BRL", newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getMyOffer(newOfferId);
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit());
assertEquals(brlAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("BRL", newOffer.getCounterCurrencyCode());
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) {
assertTrue(() -> {
String counterCurrencyCode = offer.getCounterCurrencyCode();
double mktPrice = aliceClient.getBtcPrice(counterCurrencyCode);
double scaledOfferPrice = getScaledOfferPrice(offer.getPrice(), counterCurrencyCode);
double priceAsDouble = Double.parseDouble(offer.getPrice());
double expectedDiffPct = scaleDownByPowerOf10(priceMarginPctInput, 2);
double actualDiffPct = offer.getDirection().equals(BUY.name())
? getPercentageDifference(scaledOfferPrice, mktPrice)
: getPercentageDifference(mktPrice, scaledOfferPrice);
? getPercentageDifference(priceAsDouble, mktPrice)
: getPercentageDifference(mktPrice, priceAsDouble);
double pctDiffDelta = abs(expectedDiffPct) - abs(actualDiffPct);
return isCalculatedPriceWithinErrorTolerance(pctDiffDelta,
expectedDiffPct,
actualDiffPct,
mktPrice,
scaledOfferPrice,
priceAsDouble,
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,
double expectedDiffPct,
double actualDiffPct,
@ -245,7 +314,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
actualDiffPct,
mktPrice,
scaledOfferPrice);
log.warn(offer.toString());
log.trace(offer.toString());
}
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 static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OfferDirection.BUY;
@Disabled
@Slf4j
@ -52,7 +51,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
100000000000L, // exceeds amount limit
100000000000L,
"10000.0000",
getDefaultBuyerSecurityDepositAsPercent(),
defaultBuyerSecurityDepositPct.get(),
usdAccount.getId()));
assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage());
}
@ -68,7 +67,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
10000000L,
10000000L,
"40000.0000",
getDefaultBuyerSecurityDepositAsPercent(),
defaultBuyerSecurityDepositPct.get(),
chfAccount.getId()));
String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId());
assertEquals(expectedError, exception.getMessage());
@ -85,7 +84,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
10000000L,
10000000L,
"63000.0000",
getDefaultBuyerSecurityDepositAsPercent(),
defaultBuyerSecurityDepositPct.get(),
audAccount.getId()));
String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId());
assertEquals(expectedError, exception.getMessage());

View File

@ -1,6 +1,7 @@
package bisq.apitest.method.payment;
import bisq.core.api.model.PaymentAccountForm;
import bisq.core.locale.FiatCurrency;
import bisq.core.locale.Res;
import bisq.core.locale.TradeCurrency;
import bisq.core.payment.PaymentAccount;
@ -17,10 +18,13 @@ import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
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_NUMBER = "bankAccountNumber";
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_NAME = "bankBranchName";
static final String PROPERTY_NAME_BANK_CODE = "bankCode";
static final String PROPERTY_NAME_BANK_COUNTRY_CODE = "bankCountryCode";
@SuppressWarnings("unused")
static final String PROPERTY_NAME_BANK_ID = "bankId";
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_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_CITY = "city";
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_TAX_ID = "holderTaxId";
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_NATIONAL_ACCOUNT_ID = "nationalAccountId";
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_REQUIREMENTS = "requirements";
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_SPECIAL_INSTRUCTIONS = "specialInstructions";
static final String PROPERTY_NAME_STATE = "state";
static final String PROPERTY_NAME_TRADE_CURRENCIES = "tradeCurrencies";
static final String PROPERTY_NAME_USERNAME = "userName";
@ -110,7 +130,7 @@ public class AbstractPaymentAccountTest extends MethodTest {
COMPLETED_FORM_MAP.clear();
File emptyForm = getPaymentAccountForm(aliceClient, paymentMethodId);
// A short cut over the API:
// A shortcut over the API:
// File emptyForm = PAYMENT_ACCOUNT_FORM.getPaymentAccountForm(paymentMethodId);
log.debug("{} Empty form saved to {}",
testName(testInfo),
@ -125,7 +145,13 @@ public class AbstractPaymentAccountTest extends MethodTest {
PAYMENT_ACCOUNT_FORM.toJsonString(jsonForm),
Object.class);
assertNotNull(emptyForm);
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("your accountname", emptyForm.get(PROPERTY_NAME_ACCOUNT_NAME));
for (String field : fields) {
@ -149,6 +175,15 @@ public class AbstractPaymentAccountTest extends MethodTest {
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,
PaymentAccount paymentAccount) {
assertNotNull(paymentAccount.getTradeCurrencies());
@ -164,14 +199,36 @@ public class AbstractPaymentAccountTest extends MethodTest {
assertTrue(paymentAccount.isPresent());
}
protected final String getCompletedFormAsJsonString() {
File completedForm = fillPaymentAccountForm();
protected final String getCompletedFormAsJsonString(List<String> comments) {
File completedForm = fillPaymentAccountForm(comments);
String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
log.debug("Completed form: {}", 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;
try {
tmpJsonForm = File.createTempFile("temp_acct_form_",
@ -182,7 +239,7 @@ public class AbstractPaymentAccountTest extends MethodTest {
writer.name(PROPERTY_NAME_JSON_COMMENTS);
writer.beginArray();
for (String s : PROPERTY_VALUE_JSON_COMMENTS) {
for (String s : comments) {
writer.value(s);
}
writer.endArray();

View File

@ -17,12 +17,13 @@
package bisq.apitest.method.payment;
import bisq.core.locale.FiatCurrency;
import bisq.core.locale.TradeCurrency;
import bisq.core.payment.AdvancedCashAccount;
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.ChaseQuickPayAccount;
import bisq.core.payment.ClearXchangeAccount;
import bisq.core.payment.F2FAccount;
import bisq.core.payment.FasterPaymentsAccount;
@ -32,7 +33,9 @@ import bisq.core.payment.JapanBankAccount;
import bisq.core.payment.MoneyBeamAccount;
import bisq.core.payment.MoneyGramAccount;
import bisq.core.payment.NationalBankAccount;
import bisq.core.payment.PaxumAccount;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.PayseraAccount;
import bisq.core.payment.PerfectMoneyAccount;
import bisq.core.payment.PopmoneyAccount;
import bisq.core.payment.PromptPayAccount;
@ -41,6 +44,7 @@ import bisq.core.payment.SameBankAccount;
import bisq.core.payment.SepaAccount;
import bisq.core.payment.SepaInstantAccount;
import bisq.core.payment.SpecificBanksAccount;
import bisq.core.payment.SwiftAccount;
import bisq.core.payment.SwishAccount;
import bisq.core.payment.TransferwiseAccount;
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.SameBankAccountPayload;
import bisq.core.payment.payload.SpecificBanksAccountPayload;
import bisq.core.payment.payload.SwiftAccountPayload;
import io.grpc.StatusRuntimeException;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@ -70,16 +77,23 @@ import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
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.cli.TableFormat.formatPaymentAcctTbl;
import static bisq.core.locale.CurrencyUtil.*;
import static bisq.cli.table.builder.TableType.PAYMENT_ACCOUNT_TBL;
import static bisq.core.locale.CurrencyUtil.getAllSortedFiatCurrencies;
import static bisq.core.locale.CurrencyUtil.getTradeCurrency;
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.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import bisq.cli.table.builder.TableBuilder;
@SuppressWarnings({"OptionalGetWithoutIsPresent", "ConstantConditions"})
@Disabled
@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_ACCOUNT_NAME, "Advanced Cash Acct");
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"));
String jsonString = getCompletedFormAsJsonString();
AdvancedCashAccount paymentAccount = (AdvancedCashAccount) createPaymentAccount(aliceClient, jsonString);
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);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
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_SALT, encodeToHex("Restored Australia Pay ID Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
AustraliaPayid paymentAccount = (AustraliaPayid) createPaymentAccount(aliceClient, jsonString);
AustraliaPayidAccount paymentAccount = (AustraliaPayidAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("AUD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
@ -156,6 +177,33 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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
public void testCreateCashDepositAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, CASH_DEPOSIT_ID);
@ -189,7 +237,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString();
CashDepositAccount paymentAccount = (CashDepositAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
@ -253,28 +301,6 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
print(paymentAccount);
}
@Test
public void testCreateChaseQuickPayAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, CHASE_QUICK_PAY_ID);
verifyEmptyForm(emptyForm,
CHASE_QUICK_PAY_ID,
PROPERTY_NAME_EMAIL,
PROPERTY_NAME_HOLDER_NAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CHASE_QUICK_PAY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Quick Pay Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "johndoe@quickpay.com");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
ChaseQuickPayAccount paymentAccount = (ChaseQuickPayAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
print(paymentAccount);
}
@Test
public void testCreateClearXChangeAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, CLEAR_X_CHANGE_ID);
@ -290,7 +316,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString();
ClearXchangeAccount paymentAccount = (ClearXchangeAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL_OR_MOBILE_NR), paymentAccount.getEmailOrMobileNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
@ -363,7 +389,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString();
HalCashAccount paymentAccount = (HalCashAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr());
print(paymentAccount);
@ -448,7 +474,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString();
MoneyBeamAccount paymentAccount = (MoneyBeamAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
@ -466,6 +492,11 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
PROPERTY_NAME_STATE);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_GRAM_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Gram Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_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_EMAIL, "john@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "US");
@ -474,7 +505,9 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString();
MoneyGramAccount paymentAccount = (MoneyGramAccount) createPaymentAccount(aliceClient, jsonString);
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);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
@ -497,13 +530,65 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString();
PerfectMoneyAccount paymentAccount = (PerfectMoneyAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void 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
public void testCreatePopmoneyAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, POPMONEY_ID);
@ -519,7 +604,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString();
PopmoneyAccount paymentAccount = (PopmoneyAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
@ -554,12 +639,19 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
PROPERTY_NAME_USERNAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, REVOLUT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Revolut Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_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_SALT, "");
String jsonString = getCompletedFormAsJsonString();
RevolutAccount paymentAccount = (RevolutAccount) createPaymentAccount(aliceClient, jsonString);
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);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_USERNAME), paymentAccount.getUserName());
print(paymentAccount);
@ -631,7 +723,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban());
@ -662,7 +754,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban());
@ -720,6 +812,64 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
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
public void testCreateSwishAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, SWISH_ID);
@ -751,17 +901,16 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, "eur");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_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_SALT, "");
String jsonString = getCompletedFormAsJsonString();
TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
assertEquals(1, paymentAccount.getTradeCurrencies().size());
TradeCurrency expectedCurrency = getTradeCurrency("EUR").get();
assertEquals(expectedCurrency, paymentAccount.getSelectedTradeCurrency());
List<TradeCurrency> expectedTradeCurrencies = singletonList(expectedCurrency);
verifyAccountTradeCurrencies(expectedTradeCurrencies, paymentAccount);
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);
@ -775,7 +924,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, "ars, cad, hrk, czk, eur, hkd, idr, jpy, chf, nzd");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_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_SALT, "");
String jsonString = getCompletedFormAsJsonString();
@ -787,7 +937,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
add(getTradeCurrency("CAD").get());
add(getTradeCurrency("HRK").get());
add(getTradeCurrency("CZK").get());
add(getTradeCurrency("EUR").get());
add(getTradeCurrency(EUR).get());
add(getTradeCurrency("HKD").get());
add(getTradeCurrency("IDR").get());
add(getTradeCurrency("JPY").get());
@ -795,8 +945,34 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
add(getTradeCurrency("NZD").get());
}};
verifyAccountTradeCurrencies(expectedTradeCurrencies, paymentAccount);
TradeCurrency expectedSelectedCurrency = expectedTradeCurrencies.get(0);
assertEquals(expectedSelectedCurrency, paymentAccount.getSelectedTradeCurrency());
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 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);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
print(paymentAccount);
@ -836,7 +1012,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
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());
}
@ -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_ACCOUNT_NAME, "Uphold Acct");
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"));
String jsonString = getCompletedFormAsJsonString();
UpholdAccount paymentAccount = (UpholdAccount) createPaymentAccount(aliceClient, jsonString);
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);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
@ -875,7 +1058,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString();
USPostalMoneyOrderAccount paymentAccount = (USPostalMoneyOrderAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_POSTAL_ADDRESS), paymentAccount.getPostalAddress());
@ -923,7 +1106,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString();
WesternUnionAccount paymentAccount = (WesternUnionAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity());
@ -942,7 +1125,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
private void print(PaymentAccount paymentAccount) {
if (log.isDebugEnabled()) {
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 java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.slf4j.Logger;
import lombok.Getter;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.TestInfo;
import static bisq.cli.TradeFormat.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL;
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.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.cli.CliMain;
import bisq.cli.GrpcClient;
import bisq.cli.table.builder.TableBuilder;
public class AbstractTradeTest extends AbstractOfferTest {
public static final ExpectedProtocolStatus EXPECTED_PROTOCOL_STATUS = new ExpectedProtocolStatus();
// A Trade ID cache for use in @Test sequences.
@Getter
protected static String tradeId;
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
public static void initStaticFixtures() {
@ -32,13 +51,129 @@ public class AbstractTradeTest extends AbstractOfferTest {
protected final TradeInfo takeAlicesOffer(String offerId,
String paymentAccountId) {
return bobClient.takeOffer(offerId, paymentAccountId);
return takeAlicesOffer(offerId,
paymentAccountId,
true);
}
@SuppressWarnings("unused")
protected final TradeInfo takeBobsOffer(String offerId,
String paymentAccountId) {
return aliceClient.takeOffer(offerId, paymentAccountId);
protected final TradeInfo takeAlicesOffer(String offerId,
String paymentAccountId,
boolean generateBtcBlock) {
@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) {
@ -49,35 +184,54 @@ public class AbstractTradeTest extends AbstractOfferTest {
if (!isLongRunningTest)
assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositPublished, trade.getIsDepositPublished());
assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositUnlocked, trade.getIsDepositUnlocked());
assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentSent, trade.getIsPaymentSent());
assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentReceived, trade.getIsPaymentReceived());
assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositConfirmed, trade.getIsDepositUnlocked());
assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentStartedMessageSent, trade.getIsPaymentSent());
assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentReceivedMessageSent, trade.getIsPaymentReceived());
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,
TestInfo testInfo,
String description,
TradeInfo trade) {
logTrade(log, testInfo, description, trade, false);
if (log.isDebugEnabled()) {
log.debug(format("%s %s%n%s",
testName(testInfo),
description,
new TableBuilder(TRADE_DETAIL_TBL, trade).build()));
}
}
protected final void logTrade(Logger log,
TestInfo testInfo,
String description,
TradeInfo trade,
boolean force) {
if (force)
log.info(String.format("%s %s%n%s",
testName(testInfo),
description.toUpperCase(),
format(trade)));
else if (log.isDebugEnabled()) {
log.debug(String.format("%s %s%n%s",
testName(testInfo),
description.toUpperCase(),
format(trade)));
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.Phase phase;
boolean isDepositPublished;
boolean isDepositUnlocked;
boolean isPaymentSent;
boolean isPaymentReceived;
boolean isDepositConfirmed;
boolean isPaymentStartedMessageSent;
boolean isPaymentReceivedMessageSent;
boolean isPayoutPublished;
boolean isWithdrawn;
boolean isCompleted;
public ExpectedProtocolStatus setState(Trade.State state) {
this.state = state;
@ -31,18 +31,18 @@ public class ExpectedProtocolStatus {
return this;
}
public ExpectedProtocolStatus setDepositUnlocked(boolean depositUnlocked) {
isDepositUnlocked = depositUnlocked;
public ExpectedProtocolStatus setDepositConfirmed(boolean depositConfirmed) {
isDepositConfirmed = depositConfirmed;
return this;
}
public ExpectedProtocolStatus setFiatSent(boolean paymentSent) {
isPaymentSent = paymentSent;
public ExpectedProtocolStatus setPaymentStartedMessageSent(boolean paymentStartedMessageSent) {
isPaymentStartedMessageSent = paymentStartedMessageSent;
return this;
}
public ExpectedProtocolStatus setFiatReceived(boolean paymentReceived) {
isPaymentReceived = paymentReceived;
public ExpectedProtocolStatus setPaymentReceivedMessageSent(boolean paymentReceivedMessageSent) {
isPaymentReceivedMessageSent = paymentReceivedMessageSent;
return this;
}
@ -51,8 +51,8 @@ public class ExpectedProtocolStatus {
return this;
}
public ExpectedProtocolStatus setWithdrawn(boolean withdrawn) {
isWithdrawn = withdrawn;
public ExpectedProtocolStatus setCompleted(boolean completed) {
isCompleted = completed;
return this;
}
@ -60,10 +60,10 @@ public class ExpectedProtocolStatus {
state = null;
phase = null;
isDepositPublished = false;
isDepositUnlocked = false;
isPaymentSent = false;
isPaymentReceived = false;
isDepositConfirmed = false;
isPaymentStartedMessageSent = false;
isPaymentReceivedMessageSent = 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.proto.grpc.TradeInfo;
import io.grpc.StatusRuntimeException;
import java.util.function.Predicate;
import lombok.extern.slf4j.Slf4j;
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.TestMethodOrder;
import static bisq.cli.TableFormat.formatBalancesTbls;
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.apitest.config.ApiTestConfig.BTC;
import static bisq.apitest.config.ApiTestConfig.USD;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
import static bisq.core.trade.Trade.State.*;
import static java.lang.String.format;
import static bisq.core.trade.Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG;
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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_RESERVED;
import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OfferDirection.BUY;
import static protobuf.OpenOffer.State.AVAILABLE;
@Disabled
@ -61,62 +54,35 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
try {
PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US");
var alicesOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
"usd",
USD,
12_500_000L,
12_500_000L, // min-amount = amount
0.00,
getDefaultBuyerSecurityDepositAsPercent(),
alicesUsdAccount.getId());
defaultBuyerSecurityDepositPct.get(),
alicesUsdAccount.getId(),
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId();
// Wait for Alice's AddToOfferBook task.
// Wait times vary; my logs show >= 2 second delay.
sleep(3000); // TODO loop instead of hard code wait time
var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), "usd");
// Wait times vary; my logs show >= 2-second delay.
sleep(3_000); // TODO loop instead of hard code a wait time
var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), USD);
assertEquals(1, alicesUsdOffers.size());
PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US");
var trade = takeAlicesOffer(offerId, bobsUsdAccount.getId());
assertNotNull(trade);
assertEquals(offerId, trade.getTradeId());
// Cache the trade id for the other tests.
tradeId = trade.getTradeId();
genBtcBlocksThenWait(1, 4000);
alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), "usd");
var trade = takeAlicesOffer(offerId,
bobsUsdAccount.getId(),
false);
sleep(2_500); // Allow available offer to be removed from offer book.
alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), USD);
assertEquals(0, alicesUsdOffers.size());
genBtcBlocksThenWait(1, 2_500);
waitForDepositConfirmation(log, testInfo, bobClient, trade.getTradeId());
genBtcBlocksThenWait(1, 2500);
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
trade = bobClient.getTrade(trade.getTradeId());
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()));
}
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);
}
@ -127,56 +93,10 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) {
try {
var trade = aliceClient.getTrade(tradeId);
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()));
}
waitForDepositConfirmation(log, testInfo, aliceClient, trade.getTradeId());
aliceClient.confirmPaymentStarted(trade.getTradeId());
sleep(6000);
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;
}
}
sleep(6_000);
waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId);
} catch (StatusRuntimeException e) {
fail(e);
}
@ -186,82 +106,19 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
@Order(3)
public void testBobsConfirmPaymentReceived(final TestInfo testInfo) {
try {
waitForSellerSeesPaymentInitiatedMessage(log, testInfo, bobClient, 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());
sleep(3000);
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)
.setFiatReceived(true);
.setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after confirming fiat payment received", trade);
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(4)
public void testAlicesKeepFunds(final TestInfo testInfo) {
try {
genBtcBlocksThenWait(1, 1000);
var trade = aliceClient.getTrade(tradeId);
logTrade(log, testInfo, "Alice's view before keeping funds", trade);
aliceClient.keepFunds(tradeId);
genBtcBlocksThenWait(1, 1000);
trade = aliceClient.getTrade(tradeId);
EXPECTED_PROTOCOL_STATUS.setState(BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG)
.setPhase(PAYOUT_PUBLISHED);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Alice's view after keeping funds", trade);
logTrade(log, testInfo, "Alice's Maker/Buyer View (Done)", aliceClient.getTrade(tradeId), true);
logTrade(log, testInfo, "Bob's Taker/Seller View (Done)", bobClient.getTrade(tradeId), true);
var alicesBalances = aliceClient.getBalances();
log.info("{} Alice's Current Balance:\n{}",
testName(testInfo),
formatBalancesTbls(alicesBalances));
var bobsBalances = bobClient.getBalances();
log.info("{} Bob's Current Balance:\n{}",
testName(testInfo),
formatBalancesTbls(bobsBalances));
} catch (StatusRuntimeException e) {
fail(e);
}

View File

@ -0,0 +1,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.proto.grpc.TradeInfo;
import io.grpc.StatusRuntimeException;
import java.util.function.Predicate;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
@ -35,20 +31,16 @@ import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.cli.TableFormat.formatBalancesTbls;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static bisq.core.trade.Trade.Phase.DEPOSIT_UNLOCKED;
import static bisq.core.trade.Trade.Phase.PAYMENT_SENT;
import static bisq.apitest.config.ApiTestConfig.USD;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
import static bisq.core.trade.Trade.Phase.WITHDRAWN;
import static bisq.core.trade.Trade.State.*;
import static java.lang.String.format;
import static 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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_RESERVED;
import static protobuf.OfferPayload.Direction.SELL;
import static protobuf.OpenOffer.State.AVAILABLE;
import static protobuf.OfferDirection.SELL;
@Disabled
@Slf4j
@ -57,6 +49,9 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
// Alice is maker/seller, Bob is taker/buyer.
// Maker and Taker fees are in BTC.
private static final String TRADE_FEE_CURRENCY_CODE = BTC;
private static final String WITHDRAWAL_TX_MEMO = "Bob's trade withdrawal";
@Test
@ -65,63 +60,35 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
try {
PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US");
var alicesOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(),
"usd",
USD,
12_500_000L,
12_500_000L, // min-amount = amount
0.00,
getDefaultBuyerSecurityDepositAsPercent(),
alicesUsdAccount.getId());
defaultBuyerSecurityDepositPct.get(),
alicesUsdAccount.getId(),
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId();
// 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.
sleep(3000); // TODO loop instead of hard code wait time
var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(SELL.name(), "usd");
sleep(3_000); // TODO loop instead of hard code a wait time
var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(SELL.name(), USD);
assertEquals(1, alicesUsdOffers.size());
PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US");
var trade = takeAlicesOffer(offerId, bobsUsdAccount.getId());
assertNotNull(trade);
assertEquals(offerId, trade.getTradeId());
// Cache the trade id for the other tests.
tradeId = trade.getTradeId();
genBtcBlocksThenWait(1, 4000);
var takeableUsdOffers = bobClient.getOffersSortedByDate(SELL.name(), "usd");
var trade = takeAlicesOffer(offerId,
bobsUsdAccount.getId(),
false);
sleep(2_500); // Allow available offer to be removed from offer book.
var takeableUsdOffers = bobClient.getOffersSortedByDate(SELL.name(), USD);
assertEquals(0, takeableUsdOffers.size());
genBtcBlocksThenWait(1, 2500);
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
trade = bobClient.getTrade(trade.getTradeId());
if (!trade.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()));
}
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);
}
@ -132,54 +99,10 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
public void testBobsConfirmPaymentStarted(final TestInfo testInfo) {
try {
var trade = bobClient.getTrade(tradeId);
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()));
}
verifyTakerDepositConfirmed(trade);
bobClient.confirmPaymentStarted(tradeId);
sleep(6000);
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;
}
}
sleep(6_000);
waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId);
} catch (StatusRuntimeException e) {
fail(e);
}
@ -189,83 +112,21 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
@Order(3)
public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) {
try {
waitForSellerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, 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());
sleep(3000);
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)
.setFiatReceived(true);
.setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade);
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(4)
public void testBobsBtcWithdrawalToExternalAddress(final TestInfo testInfo) {
try {
genBtcBlocksThenWait(1, 1000);
var trade = bobClient.getTrade(tradeId);
logTrade(log, testInfo, "Bob's view before withdrawing funds to external wallet", trade);
String toAddress = bitcoinCli.getNewBtcAddress();
bobClient.withdrawFunds(tradeId, toAddress, WITHDRAWAL_TX_MEMO);
genBtcBlocksThenWait(1, 1000);
trade = bobClient.getTrade(tradeId);
EXPECTED_PROTOCOL_STATUS.setState(WITHDRAW_COMPLETED)
.setPhase(WITHDRAWN)
.setWithdrawn(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after withdrawing BTC funds to external wallet", trade);
logTrade(log, testInfo, "Alice's Maker/Buyer View (Done)", aliceClient.getTrade(tradeId), true);
logTrade(log, testInfo, "Bob's Taker/Seller View (Done)", bobClient.getTrade(tradeId), true);
var alicesBalances = aliceClient.getBalances();
log.info("{} Alice's Current Balance:\n{}",
testName(testInfo),
formatBalancesTbls(alicesBalances));
var bobsBalances = bobClient.getBalances();
log.info("{} Bob's Current Balance:\n{}",
testName(testInfo),
formatBalancesTbls(bobsBalances));
} catch (StatusRuntimeException e) {
fail(e);
}
}
}

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
@Slf4j
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.
public static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES =
bisq.core.api.model.BtcBalanceInfo.valueOf(1000000000,
@ -17,7 +17,6 @@ public class WalletTestUtil {
1000000000,
0);
public static void verifyBtcBalances(bisq.core.api.model.BtcBalanceInfo expected,
BtcBalanceInfo actual) {
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.testAlicesConfirmPaymentStarted(testInfo);
test.testBobsConfirmPaymentReceived(testInfo);
test.testAlicesKeepFunds(testInfo);
}
public void testTakeSellBTCOffer(final TestInfo testInfo) {
@ -80,7 +79,6 @@ public class LongRunningTradesTest extends AbstractTradeTest {
test.testTakeAlicesSellOffer(testInfo);
test.testBobsConfirmPaymentStarted(testInfo);
test.testAlicesConfirmPaymentReceived(testInfo);
test.testBobsBtcWithdrawalToExternalAddress(testInfo);
}
protected static boolean envLongRunningTestEnabled() {

View File

@ -20,6 +20,7 @@ package bisq.apitest.scenario;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
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.CreateOfferUsingFixedPriceTest;
import bisq.apitest.method.offer.CreateOfferUsingMarketPriceMarginTest;
import bisq.apitest.method.offer.CreateXMROffersTest;
import bisq.apitest.method.offer.ValidateCreateOfferTest;
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OfferTest extends AbstractOfferTest {
@BeforeAll
public static void setUp() {
setUp(false); // Use setUp(true) for running API daemons in remote debug mode.
}
@Test
@Order(1)
public void testAmtTooLargeShouldThrowException() {
public void testCreateOfferValidation() {
ValidateCreateOfferTest test = new ValidateCreateOfferTest();
test.testAmtTooLargeShouldThrowException();
test.testNoMatchingEURPaymentAccountShouldThrowException();
@ -57,18 +64,32 @@ public class OfferTest extends AbstractOfferTest {
@Order(3)
public void testCreateOfferUsingFixedPrice() {
CreateOfferUsingFixedPriceTest test = new CreateOfferUsingFixedPriceTest();
test.testCreateAUDXMRBuyOfferUsingFixedPrice16000();
test.testCreateUSDXMRBuyOfferUsingFixedPrice100001234();
test.testCreateEURXMRSellOfferUsingFixedPrice95001234();
test.testCreateAUDBTCBuyOfferUsingFixedPrice16000();
test.testCreateUSDBTCBuyOfferUsingFixedPrice100001234();
test.testCreateEURBTCSellOfferUsingFixedPrice95001234();
}
@Test
@Order(4)
public void testCreateOfferUsingMarketPriceMargin() {
public void testCreateOfferUsingMarketPriceMarginPct() {
CreateOfferUsingMarketPriceMarginTest test = new CreateOfferUsingMarketPriceMarginTest();
test.testCreateUSDXMRBuyOffer5PctPriceMargin();
test.testCreateNZDXMRBuyOfferMinus2PctPriceMargin();
test.testCreateGBPXMRSellOfferMinus1Point5PctPriceMargin();
test.testCreateBRLXMRSellOffer6Point55PctPriceMargin();
test.testCreateUSDBTCBuyOffer5PctPriceMargin();
test.testCreateNZDBTCBuyOfferMinus2PctPriceMargin();
test.testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin();
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.testCreateAliPayAccount(testInfo);
test.testCreateAustraliaPayidAccount(testInfo);
test.testCreateCapitualAccount(testInfo);
test.testCreateCashDepositAccount(testInfo);
test.testCreateBrazilNationalBankAccount(testInfo);
test.testCreateChaseQuickPayAccount(testInfo);
test.testCreateClearXChangeAccount(testInfo);
test.testCreateF2FAccount(testInfo);
test.testCreateFasterPaymentsAccount(testInfo);
@ -61,6 +61,8 @@ public class PaymentAccountTest extends AbstractPaymentAccountTest {
test.testCreateMoneyBeamAccount(testInfo);
test.testCreateMoneyGramAccount(testInfo);
test.testCreatePerfectMoneyAccount(testInfo);
test.testCreatePaxumAccount(testInfo);
test.testCreatePayseraAccount(testInfo);
test.testCreatePopmoneyAccount(testInfo);
test.testCreatePromptPayAccount(testInfo);
test.testCreateRevolutAccount(testInfo);
@ -68,12 +70,12 @@ public class PaymentAccountTest extends AbstractPaymentAccountTest {
test.testCreateSepaInstantAccount(testInfo);
test.testCreateSepaAccount(testInfo);
test.testCreateSpecificBanksAccount(testInfo);
test.testCreateSwiftAccount(testInfo);
test.testCreateSwishAccount(testInfo);
// TransferwiseAccount is only PaymentAccount with a
// tradeCurrencies field in the json form.
test.testCreateTransferwiseAccountWith1TradeCurrency(testInfo);
test.testCreateTransferwiseAccountWith10TradeCurrencies(testInfo);
test.testCreateTransferwiseAccountWithSupportedTradeCurrencies(testInfo);
test.testCreateTransferwiseAccountWithInvalidBrlTradeCurrencyShouldThrowException(testInfo);
test.testCreateTransferwiseAccountWithoutTradeCurrenciesShouldThrowException(testInfo);

View File

@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
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.arbdaemon;
import static bisq.apitest.config.HavenoAppConfig.seednode;
@ -54,7 +55,7 @@ public class StartupTest extends MethodTest {
@BeforeAll
public static void setUp() {
try {
callRateMeteringConfigFile = defaultRateMeterInterceptorConfig();
callRateMeteringConfigFile = getTestRateMeterInterceptorConfig();
startSupportingApps(callRateMeteringConfigFile,
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.TakeBuyBTCOfferTest;
import bisq.apitest.method.trade.TakeBuyBTCOfferWithNationalBankAcctTest;
import bisq.apitest.method.trade.TakeBuyXMROfferTest;
import bisq.apitest.method.trade.TakeSellBTCOfferTest;
import bisq.apitest.method.trade.TakeSellXMROfferTest;
@Slf4j
@ -49,7 +52,6 @@ public class TradeTest extends AbstractTradeTest {
test.testTakeAlicesBuyOffer(testInfo);
test.testAlicesConfirmPaymentStarted(testInfo);
test.testBobsConfirmPaymentReceived(testInfo);
test.testAlicesKeepFunds(testInfo);
}
@Test
@ -59,6 +61,35 @@ public class TradeTest extends AbstractTradeTest {
test.testTakeAlicesSellOffer(testInfo);
test.testBobsConfirmPaymentStarted(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.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.System.getProperty;
import static java.nio.file.Files.readAllBytes;
@ -74,7 +74,7 @@ public abstract class AbstractBotTest extends MethodTest {
} else {
throw new UnsupportedOperationException(
format("This test harness bot does not work with %s payment accounts yet.",
getPaymentMethodById(paymentMethodId).getDisplayString()));
getPaymentMethod(paymentMethodId).getDisplayString()));
}
} else {
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.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.util.concurrent.TimeUnit.MINUTES;
@ -62,7 +62,7 @@ class Bot {
} else {
throw new UnsupportedOperationException(
format("This bot test does not work with %s payment accounts yet.",
getPaymentMethodById(paymentMethodId).getDisplayString()));
getPaymentMethod(paymentMethodId).getDisplayString()));
}
} else {
Country country = findCountry(botScript.getCountryCode());

View File

@ -39,11 +39,6 @@ import bisq.cli.GrpcClient;
/**
* Convenience GrpcClient wrapper for bots using gRPC services.
*
* TODO Consider if the duplication smell is bad enough to force a BotClient user
* to use the GrpcClient instead (and delete this class). But right now, I think it is
* OK because moving some of the non-gRPC related methods to GrpcClient is even smellier.
*
*/
@SuppressWarnings({"JavaDoc", "unused"})
@Slf4j
@ -124,6 +119,8 @@ public class BotClient {
* @param minAmountInSatoshis
* @param priceMarginAsPercent
* @param securityDepositAsPercent
* @param feeCurrency
* @param triggerPrice
* @return OfferInfo
*/
public OfferInfo createOfferAtMarketBasedPrice(PaymentAccount paymentAccount,
@ -132,14 +129,16 @@ public class BotClient {
long amountInSatoshis,
long minAmountInSatoshis,
double priceMarginAsPercent,
double securityDepositAsPercent) {
double securityDepositAsPercent,
String triggerPrice) {
return grpcClient.createMarketBasedPricedOffer(direction,
currencyCode,
amountInSatoshis,
minAmountInSatoshis,
priceMarginAsPercent,
securityDepositAsPercent,
paymentAccount.getId());
paymentAccount.getId(),
triggerPrice);
}
/**
@ -151,6 +150,7 @@ public class BotClient {
* @param minAmountInSatoshis
* @param fixedOfferPriceAsString
* @param securityDepositAsPercent
* @param feeCurrency
* @return OfferInfo
*/
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
* @return boolean
*/
public boolean isTakerDepositFeeTxUnlocked(String tradeId) {
public boolean isTakerDepositFeeTxConfirmed(String tradeId) {
return grpcClient.getTrade(tradeId).getIsDepositUnlocked();
}
@ -278,15 +278,6 @@ public class BotClient {
grpcClient.confirmPaymentReceived(tradeId);
}
/**
* Sends a 'keep funds in wallet message' for a trade with the given tradeId,
* or throws an exception.
* @param tradeId
*/
public void sendKeepFundsMessage(String tradeId) {
grpcClient.keepFunds(tradeId);
}
/**
* Create and save a new PaymentAccount with details in the given json.
* @param json

View File

@ -33,10 +33,10 @@ import java.util.function.Supplier;
import lombok.Getter;
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.common.util.MathUtils.scaleDownByPowerOf10;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static bisq.core.payment.payload.PaymentMethod.F2F_ID;
import static java.lang.String.format;
import static java.math.RoundingMode.HALF_UP;
@ -124,7 +124,8 @@ public class RandomOffer {
amount,
minAmount,
priceMargin,
getDefaultBuyerSecurityDepositAsPercent());
defaultBuyerSecurityDepositPct.get(),
"0" /*no trigger price*/);
} else {
this.offer = botClient.createOfferAtFixedPrice(paymentAccount,
direction,
@ -132,7 +133,7 @@ public class RandomOffer {
amount,
minAmount,
fixedOfferPrice,
getDefaultBuyerSecurityDepositAsPercent());
defaultBuyerSecurityDepositPct.get());
}
this.id = offer.getId();
return this;
@ -162,11 +163,11 @@ public class RandomOffer {
log.info(description);
if (useMarketBasedPrice) {
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 {
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.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;
@ -34,6 +34,7 @@ import bisq.apitest.scenario.bot.protocol.TakerBotProtocol;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.script.BotScript;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import bisq.cli.table.builder.TableBuilder;
@Slf4j
public
@ -74,10 +75,14 @@ class RobotBob extends Bot {
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{}",
++numTrades,
numTrades == 1 ? "" : "s",
formatBalancesTbls(botClient.getBalance()));
balancesBuilder);
if (numTrades < actions.length) {
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.shutdown.ManualShutdown.checkIfShutdownCalled;
import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL;
import static java.lang.String.format;
import static java.lang.System.currentTimeMillis;
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.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import bisq.cli.TradeFormat;
import bisq.cli.table.builder.TableBuilder;
@Slf4j
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.",
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.");
createGenerateBtcBlockScript();
}
@ -133,7 +134,8 @@ public abstract class BotProtocol {
try {
var t = this.getBotClient().getTrade(trade.getTradeId());
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;
}
} catch (Exception ex) {
@ -167,7 +169,8 @@ public abstract class BotProtocol {
try {
var t = this.getBotClient().getTrade(trade.getTradeId());
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;
}
} catch (Exception ex) {
@ -202,7 +205,7 @@ public abstract class BotProtocol {
if (t.getIsPayoutPublished()) {
log.info("Payout tx {} has been published for trade:\n{}",
t.getPayoutTxId(),
TradeFormat.format(t));
new TableBuilder(TRADE_DETAIL_TBL, t).build().toString());
return t;
}
} 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) {
File script = bashScriptGenerator.createPaymentStartedScript(trade);
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) {
waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_UNLOCKED);
waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED);
}
private void waitForTakerDepositFee(String tradeId, ProtocolStep depositTxProtocolStep) {
initProtocolStep.accept(depositTxProtocolStep);
validateCurrentProtocolStep(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED, WAIT_FOR_TAKER_DEPOSIT_TX_UNLOCKED);
validateCurrentProtocolStep(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED);
try {
log.info(waitingForDepositFeeTxMsg(tradeId));
while (isWithinProtocolStepTimeLimit()) {
@ -316,8 +304,8 @@ public abstract class BotProtocol {
if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) && trade.getIsDepositPublished()) {
log.info("Taker deposit fee tx {} has been published.", trade.getTakerDepositTxId());
return true;
} else if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_UNLOCKED) && trade.getIsDepositUnlocked()) {
log.info("Taker deposit fee tx {} is unlocked.", trade.getTakerDepositTxId());
} else if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED) && trade.getIsDepositUnlocked()) {
log.info("Taker deposit fee tx {} has been confirmed.", trade.getTakerDepositTxId());
return true;
} else {
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.WAIT_FOR_OFFER_TAKER;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
import static bisq.cli.TableFormat.formatOfferTable;
import static java.util.Collections.singletonList;
import static bisq.cli.table.builder.TableType.OFFER_TBL;
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.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import bisq.cli.TradeFormat;
import bisq.cli.table.builder.TableBuilder;
@Slf4j
public class MakerBotProtocol extends BotProtocol {
@ -56,16 +56,13 @@ public class MakerBotProtocol extends BotProtocol {
: waitForPaymentStartedMessage.andThen(sendPaymentReceivedMessage);
completeFiatTransaction.apply(trade);
Function<TradeInfo, TradeInfo> closeTrade = waitForPayoutTx.andThen(keepFundsFromTrade);
closeTrade.apply(trade);
currentProtocolStep = DONE;
}
private final Supplier<OfferInfo> randomOffer = () -> {
checkIfShutdownCalled("Interrupted before creating random offer.");
OfferInfo offer = new RandomOffer(botClient, paymentAccount).create().getOffer();
log.info("Created random {} offer\n{}", currencyCode, formatOfferTable(singletonList(offer), currencyCode));
log.info("Created random {} offer\n{}", currencyCode, new TableBuilder(OFFER_TBL, offer).build());
return offer;
};
@ -98,7 +95,9 @@ public class MakerBotProtocol extends BotProtocol {
private Optional<TradeInfo> getNewTrade(String offerId) {
try {
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);
} catch (Exception ex) {
// Get trade will throw a non-fatal gRPC exception if not found.

View File

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

View File

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

View File

@ -17,8 +17,9 @@
package bisq.apitest.scenario.bot.script;
import bisq.core.util.JsonUtil;
import bisq.common.file.JsonFileManager;
import bisq.common.util.Utilities;
import joptsimple.BuiltinHelpFormatter;
import joptsimple.OptionParser;
@ -214,7 +215,7 @@ public class BotScriptGenerator {
}
private String generateBotScriptTemplate() {
return Utilities.objectToJson(new BotScript(
return JsonUtil.objectToJson(new BotScript(
useTestHarness,
botPaymentMethodId,
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'
bitcoinjVersion = '2a80db4'
codecVersion = '1.13'
cowwocVersion = '1.2'
easybindVersion = '1.0.3'
easyVersion = '4.0.1'
findbugsVersion = '3.0.2'
@ -404,6 +405,7 @@ configure(project(':cli')) {
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion")
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testImplementation "org.bitbucket.cowwoc:diff-match-patch:$cowwocVersion"
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;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.TradeInfo;
import io.grpc.StatusRuntimeException;
@ -39,18 +40,18 @@ import java.util.List;
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.toSatoshis;
import static bisq.cli.CurrencyFormat.toSecurityDepositAsPct;
import static bisq.cli.Method.*;
import static bisq.cli.TableFormat.*;
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.System.err;
import static java.lang.System.exit;
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.GetBTCMarketPriceOptionParser;
import bisq.cli.opts.GetBalanceOptionParser;
import bisq.cli.opts.GetOfferOptionParser;
import bisq.cli.opts.GetOffersOptionParser;
import bisq.cli.opts.GetPaymentAcctFormOptionParser;
import bisq.cli.opts.GetTradeOptionParser;
import bisq.cli.opts.GetTradesOptionParser;
import bisq.cli.opts.GetTransactionOptionParser;
import bisq.cli.opts.OfferIdOptionParser;
import bisq.cli.opts.RegisterDisputeAgentOptionParser;
import bisq.cli.opts.RemoveWalletPasswordOptionParser;
import bisq.cli.opts.SendBtcOptionParser;
@ -76,6 +78,7 @@ import bisq.cli.opts.SimpleMethodOptionParser;
import bisq.cli.opts.TakeOfferOptionParser;
import bisq.cli.opts.UnlockWalletOptionParser;
import bisq.cli.opts.WithdrawFundsOptionParser;
import bisq.cli.table.builder.TableBuilder;
/**
* A command-line client for the Bisq gRPC API.
@ -167,16 +170,15 @@ public class CliMain {
var balances = client.getBalances(currencyCode);
switch (currencyCode.toUpperCase()) {
case "BTC":
out.println(formatBtcBalanceInfoTbl(balances.getBtc()));
break;
case "XMR":
out.println(formatXmrBalanceInfoTbl(balances.getXmr()));
new TableBuilder(BTC_BALANCE_TBL, balances.getBtc()).build().print(out);
break;
case "":
default:
out.println(formatBalancesTbls(balances));
default: {
out.println("BTC");
new TableBuilder(BTC_BALANCE_TBL, balances.getBtc()).build().print(out);
break;
}
}
return;
}
case getaddressbalance: {
@ -187,7 +189,7 @@ public class CliMain {
}
var address = opts.getAddress();
var addressBalance = client.getAddressBalance(address);
out.println(formatAddressBalanceTbl(singletonList(addressBalance)));
new TableBuilder(ADDRESS_BALANCE_TBL, addressBalance).build().print(out);
return;
}
case getbtcprice: {
@ -198,7 +200,7 @@ public class CliMain {
}
var currencyCode = opts.getCurrencyCode();
var price = client.getBtcPrice(currencyCode);
out.println(formatMarketPrice(price));
out.println(formatInternalFiatPrice(price));
return;
}
case getfundingaddresses: {
@ -207,7 +209,7 @@ public class CliMain {
return;
}
var fundingAddresses = client.getFundingAddresses();
out.println(formatAddressBalanceTbl(fundingAddresses));
new TableBuilder(ADDRESS_BALANCE_TBL, fundingAddresses).build().print(out);
return;
}
case sendbtc: {
@ -269,7 +271,7 @@ public class CliMain {
}
var txId = opts.getTxId();
var tx = client.getTransaction(txId);
out.println(TransactionFormat.format(tx));
new TableBuilder(TRANSACTION_TBL, tx).build().print(out);
return;
}
case createoffer: {
@ -285,18 +287,21 @@ public class CliMain {
var minAmount = toSatoshis(opts.getMinAmount());
var useMarketBasedPrice = opts.isUsingMktPriceMargin();
var fixedPrice = opts.getFixedPrice();
var marketPriceMargin = opts.getMktPriceMarginAsBigDecimal();
var securityDeposit = toSecurityDepositAsPct(opts.getSecurityDeposit());
var offer = client.createOffer(direction,
var marketPriceMarginPct = opts.getMktPriceMarginPct();
var securityDepositPct = opts.getSecurityDepositPct();
var triggerPrice = "0"; // Cannot be defined until the new offer is added to book.
OfferInfo offer;
offer = client.createOffer(direction,
currencyCode,
amount,
minAmount,
useMarketBasedPrice,
fixedPrice,
marketPriceMargin.doubleValue(),
securityDeposit,
paymentAcctId);
out.println(formatOfferTable(singletonList(offer), currencyCode));
marketPriceMarginPct,
securityDepositPct,
paymentAcctId,
triggerPrice);
new TableBuilder(OFFER_TBL, offer).build().print(out);
return;
}
case canceloffer: {
@ -311,25 +316,25 @@ public class CliMain {
return;
}
case getoffer: {
var opts = new GetOfferOptionParser(args).parse();
var opts = new OfferIdOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var offer = client.getOffer(offerId);
out.println(formatOfferTable(singletonList(offer), offer.getCounterCurrencyCode()));
new TableBuilder(OFFER_TBL, offer).build().print(out);
return;
}
case getmyoffer: {
var opts = new GetOfferOptionParser(args).parse();
var opts = new OfferIdOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var offer = client.getMyOffer(offerId);
out.println(formatOfferTable(singletonList(offer), offer.getCounterCurrencyCode()));
new TableBuilder(OFFER_TBL, offer).build().print(out);
return;
}
case getoffers: {
@ -344,7 +349,7 @@ public class CliMain {
if (offers.isEmpty())
out.printf("no %s %s offers found%n", direction, currencyCode);
else
out.println(formatOfferTable(offers, currencyCode));
new TableBuilder(OFFER_TBL, offers).build().print(out);
return;
}
@ -360,11 +365,12 @@ public class CliMain {
if (offers.isEmpty())
out.printf("no %s %s offers found%n", direction, currencyCode);
else
out.println(formatOfferTable(offers, currencyCode));
new TableBuilder(OFFER_TBL, offers).build().print(out);
return;
}
case takeoffer: {
var opts = new TakeOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
@ -389,10 +395,30 @@ public class CliMain {
if (showContract)
out.println(trade.getContractAsJson());
else
out.println(TradeFormat.format(trade));
new TableBuilder(TRADE_DETAIL_TBL, trade).build().print(out);
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: {
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
@ -415,17 +441,6 @@ public class CliMain {
out.printf("trade %s payment received message sent%n", tradeId);
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: {
var opts = new WithdrawFundsOptionParser(args).parse();
if (opts.isForHelp()) {
@ -434,7 +449,7 @@ public class CliMain {
}
var tradeId = opts.getTradeId();
var address = opts.getAddress();
// Multi-word memos must be double quoted.
// Multi-word memos must be double-quoted.
var memo = opts.getMemo();
client.withdrawFunds(tradeId, address, memo);
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);
out.println("payment account saved");
out.println(formatPaymentAcctTbl(singletonList(paymentAccount)));
new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount).build().print(out);
return;
}
case createcryptopaymentacct: {
var opts = new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse();
var opts =
new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
@ -499,7 +515,7 @@ public class CliMain {
address,
isTradeInstant);
out.println("payment account saved");
out.println(formatPaymentAcctTbl(singletonList(paymentAccount)));
new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount).build().print(out);
return;
}
case getpaymentaccts: {
@ -509,7 +525,7 @@ public class CliMain {
}
var paymentAccounts = client.getPaymentAccounts();
if (paymentAccounts.size() > 0)
out.println(formatPaymentAcctTbl(paymentAccounts));
new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccounts).build().print(out);
else
out.println("no payment accounts are saved");
@ -585,7 +601,8 @@ public class CliMain {
}
}
} 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_]+: ", "");
if (message.equals("io exception"))
throw new RuntimeException(message + ", server may not be running", ex);
@ -666,7 +683,7 @@ public class CliMain {
stream.format(rowFormat, "------", "------", "------------");
stream.format(rowFormat, getversion.name(), "", "Get server version");
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.format(rowFormat, getaddressbalance.name(), "--address=<btc-address>", "Get server wallet address balance");
stream.println();
@ -674,11 +691,14 @@ public class CliMain {
stream.println();
stream.format(rowFormat, getfundingaddresses.name(), "", "Get BTC funding addresses");
stream.println();
stream.format(rowFormat, getunusedbsqaddress.name(), "", "Get unused BSQ address");
stream.println();
stream.format(rowFormat, "", "[--tx-fee-rate=<sats/byte>]", "");
stream.println();
stream.format(rowFormat, sendbtc.name(), "--address=<btc-address> --amount=<btc-amount> \\", "Send BTC");
stream.format(rowFormat, "", "[--tx-fee-rate=<sats/byte>]", "");
stream.format(rowFormat, "", "[--memo=<\"memo\">]", "");
stream.println();
stream.println();
stream.format(rowFormat, gettxfeerate.name(), "", "Get current tx fee rate in sats/byte");
stream.println();
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, "", "--amount=<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, "", "[--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.format(rowFormat, canceloffer.name(), "--offer-id=<offer-id>", "Cancel offer with id");
stream.println();
@ -709,22 +737,28 @@ public class CliMain {
stream.format(rowFormat, "", "--currency-code=<currency-code>", "");
stream.println();
stream.format(rowFormat, takeoffer.name(), "--offer-id=<offer-id> \\", "Take offer with id");
stream.format(rowFormat, "", "--payment-account=<payment-account-id>", "");
stream.format(rowFormat, "", "[--fee-currency=<btc>]", "");
stream.format(rowFormat, "", "[--payment-account=<payment-account-id>]", "");
stream.format(rowFormat, "", "[--fee-currency=<btc|bsq>]", "");
stream.println();
stream.format(rowFormat, gettrade.name(), "--trade-id=<trade-id> \\", "Get trade summary or full contract");
stream.format(rowFormat, "", "[--show-contract=<true|false>]", "");
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.println();
stream.format(rowFormat, confirmpaymentreceived.name(), "--trade-id=<trade-id>", "Confirm payment received");
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.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.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.println();
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.println();
stream.format(rowFormat, createcryptopaymentacct.name(), "--account-name=<name> \\", "Create a new cryptocurrency payment account");
stream.format(rowFormat, "", "--currency-code=<btc> \\", "");
stream.format(rowFormat, "", "--address=<address>", "");
stream.format(rowFormat, "", "--currency-code=<bsq> \\", "");
stream.format(rowFormat, "", "--address=<bsq-address>", "");
stream.format(rowFormat, "", "--trade-instant=<true|false>", "");
stream.println();
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.List;
class CryptoCurrencyUtil {
public class CryptoCurrencyUtil {
public static boolean isSupportedCryptoCurrency(String currencyCode) {
public static boolean apiDoesSupportCryptoCurrency(String currencyCode) {
return getSupportedCryptoCurrencies().contains(currencyCode.toUpperCase());
}
public static List<String> getSupportedCryptoCurrencies() {
final List<String> result = new ArrayList<>();
result.add("BCH");
result.sort(String::compareTo);
return result;
}

View File

@ -22,10 +22,10 @@ import bisq.proto.grpc.TxFeeRateInfo;
import com.google.common.annotations.VisibleForTesting;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.math.BigDecimal;
import java.math.BigInteger;
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.UNNECESSARY;
import monero.common.MoneroUtils;
/**
* Utility for formatting amounts, volumes and fees; there is no i18n support in the CLI.
*/
@VisibleForTesting
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 DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000");
static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,###,##0");
static final DecimalFormat SATOSHI_FORMAT = new DecimalFormat("###,##0.00000000", DECIMAL_FORMAT_SYMBOLS);
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")
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) {
return "" + MoneroUtils.atomicUnitsToXmr(amount);
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
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) {
if (txFeeRateInfo.getUseCustomTxFeeRate())
@ -72,56 +88,30 @@ public class CurrencyFormat {
formatFeeSatoshis(txFeeRateInfo.getMinFeeServiceRate()));
}
public static String formatAmountRange(long minAmount, long amount) {
return minAmount != amount
? formatSatoshis(minAmount) + " - " + formatSatoshis(amount)
: formatSatoshis(amount);
public static String formatInternalFiatPrice(BigDecimal price) {
INTERNAL_FIAT_DECIMAL_FORMAT.setMinimumFractionDigits(4);
INTERNAL_FIAT_DECIMAL_FORMAT.setMaximumFractionDigits(4);
return INTERNAL_FIAT_DECIMAL_FORMAT.format(price);
}
public static String formatVolumeRange(long minVolume, long volume) {
return minVolume != volume
? formatOfferVolume(minVolume) + " - " + formatOfferVolume(volume)
: formatOfferVolume(volume);
}
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 formatInternalFiatPrice(double price) {
US_LOCALE_NUMBER_FORMAT.setMinimumFractionDigits(4);
US_LOCALE_NUMBER_FORMAT.setMaximumFractionDigits(4);
return US_LOCALE_NUMBER_FORMAT.format(price);
}
public static String formatPrice(long price) {
NUMBER_FORMAT.setMinimumFractionDigits(4);
NUMBER_FORMAT.setMaximumFractionDigits(4);
NUMBER_FORMAT.setRoundingMode(UNNECESSARY);
return NUMBER_FORMAT.format((double) price / 10_000);
US_LOCALE_NUMBER_FORMAT.setMinimumFractionDigits(4);
US_LOCALE_NUMBER_FORMAT.setMaximumFractionDigits(4);
US_LOCALE_NUMBER_FORMAT.setRoundingMode(UNNECESSARY);
return US_LOCALE_NUMBER_FORMAT.format((double) price / 10_000);
}
public static String formatCryptoCurrencyPrice(long price) {
NUMBER_FORMAT.setMinimumFractionDigits(8);
NUMBER_FORMAT.setMaximumFractionDigits(8);
NUMBER_FORMAT.setRoundingMode(UNNECESSARY);
return NUMBER_FORMAT.format((double) price / SATOSHI_DIVISOR.doubleValue());
}
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 String formatFiatVolume(long volume) {
US_LOCALE_NUMBER_FORMAT.setMinimumFractionDigits(0);
US_LOCALE_NUMBER_FORMAT.setMaximumFractionDigits(0);
US_LOCALE_NUMBER_FORMAT.setRoundingMode(HALF_UP);
return US_LOCALE_NUMBER_FORMAT.format((double) volume / 10_000);
}
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) {
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 java.lang.String.format;
import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OfferPayload.Direction.SELL;
import static protobuf.OfferDirection.BUY;
import static protobuf.OfferDirection.SELL;
class DirectionFormat {

View File

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

View File

@ -22,16 +22,19 @@ package bisq.cli;
*/
public enum Method {
canceloffer,
closetrade,
confirmpaymentreceived,
confirmpaymentstarted,
createoffer,
editoffer,
createpaymentacct,
createcryptopaymentacct,
getaddressbalance,
getbalance,
getbtcprice,
getfundingaddresses,
getmyoffer,
@Deprecated // Since 27-Dec-2021.
getmyoffer, // Endpoint to be removed from future version. Use getoffer instead.
getmyoffers,
getoffer,
getoffers,
@ -39,10 +42,13 @@ public enum Method {
getpaymentaccts,
getpaymentmethods,
gettrade,
gettrades,
failtrade,
unfailtrade,
gettransaction,
gettxfeerate,
getunusedbsqaddress,
getversion,
keepfunds,
lockwallet,
registerdisputeagent,
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.function.Function;
import java.util.function.Predicate;
import lombok.Getter;
import static bisq.cli.opts.OptLabel.OPT_HELP;
import static java.lang.String.format;
@SuppressWarnings("unchecked")
abstract class AbstractMethodOptionParser implements MethodOpts {
// 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();
// 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();
@Getter
@ -52,7 +55,6 @@ abstract class AbstractMethodOptionParser implements MethodOpts {
public AbstractMethodOptionParser parse() {
try {
options = parser.parse(new ArgumentList(args).getMethodArguments());
//noinspection unchecked
nonOptionArguments = (List<String>) options.nonOptionArguments();
return this;
} catch (OptionException ex) {
@ -64,6 +66,17 @@ abstract class AbstractMethodOptionParser implements MethodOpts {
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) -> {
if (ex.getMessage() == null)
return null;

View File

@ -18,14 +18,7 @@
package bisq.cli.opts;
import joptsimple.OptionSpec;
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 class CancelOfferOptionParser extends OfferIdOptionParser implements MethodOpts {
public CancelOfferOptionParser(String[] args) {
super(args);
@ -34,12 +27,7 @@ public class CancelOfferOptionParser extends AbstractMethodOptionParser implemen
public CancelOfferOptionParser 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");
// Super class will short-circuit parsing if help option is present.
return this;
}

View File

@ -20,20 +20,22 @@ package bisq.cli.opts;
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_ADDRESS;
import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE;
import static bisq.cli.opts.OptLabel.OPT_TRADE_INSTANT;
import static java.lang.String.format;
public class CreateCryptoCurrencyPaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> accountNameOpt = parser.accepts(OPT_ACCOUNT_NAME, "crypto currency account name")
.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();
final OptionSpec<String> addressOpt = parser.accepts(OPT_ADDRESS, "address")
final OptionSpec<String> addressOpt = parser.accepts(OPT_ADDRESS, "altcoin address")
.withRequiredArg();
final OptionSpec<Boolean> tradeInstantOpt = parser.accepts(OPT_TRADE_INSTANT, "create trade instant account")
@ -57,6 +59,16 @@ public class CreateCryptoCurrencyPaymentAcctOptionParser extends AbstractMethodO
if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty())
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;
}

View File

@ -20,14 +20,12 @@ package bisq.cli.opts;
import joptsimple.OptionSpec;
import java.math.BigDecimal;
import static bisq.cli.opts.OptLabel.*;
import static joptsimple.internal.Strings.EMPTY;
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")
.withRequiredArg()
.defaultsTo(EMPTY);
@ -35,7 +33,7 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implemen
final OptionSpec<String> directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)")
.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();
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")
.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()
.defaultsTo("0.00");
@ -52,13 +50,14 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implemen
.withOptionalArg()
.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();
public CreateOfferOptionParser(String[] args) {
super(args);
}
@Override
public CreateOfferOptionParser parse() {
super.parse();
@ -66,9 +65,6 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implemen
if (options.has(helpOpt))
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())
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())
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");
if (options.has(mktPriceMarginOpt) && options.valueOf(mktPriceMarginOpt).isEmpty())
if (options.has(mktPriceMarginPctOpt)) {
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())
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");
else
verifyStringIsValidDouble(options.valueOf(securityDepositPctOpt));
return this;
}
@ -114,23 +120,18 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implemen
}
public boolean isUsingMktPriceMargin() {
return options.has(mktPriceMarginOpt);
return options.has(mktPriceMarginPctOpt);
}
@SuppressWarnings("unused")
public String getMktPriceMargin() {
return isUsingMktPriceMargin() ? options.valueOf(mktPriceMarginOpt) : "0.00";
}
public BigDecimal getMktPriceMarginAsBigDecimal() {
return isUsingMktPriceMargin() ? new BigDecimal(options.valueOf(mktPriceMarginOpt)) : BigDecimal.ZERO;
public double getMktPriceMarginPct() {
return isUsingMktPriceMargin() ? Double.parseDouble(options.valueOf(mktPriceMarginPctOpt)) : 0.00d;
}
public String getFixedPrice() {
return options.has(fixedPriceOpt) ? options.valueOf(fixedPriceOpt) : "0.00";
}
public String getSecurityDeposit() {
return options.valueOf(securityDepositOpt);
public double getSecurityDepositPct() {
return Double.valueOf(options.valueOf(securityDepositPctOpt));
}
}

View File

@ -28,7 +28,7 @@ public class GetOffersOptionParser extends AbstractMethodOptionParser implements
final OptionSpec<String> directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)")
.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();
public GetOffersOptionParser(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_ADDRESS = "address";
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_DIRECTION = "direction";
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_FIXED_PRICE = "fixed-price";
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_OFFER_ID = "offer-id";
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_METHOD_ID = "payment-method-id";
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_TIMEOUT = "timeout";
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_WALLET_PASSWORD = "wallet-password";
public final static String OPT_NEW_WALLET_PASSWORD = "new-wallet-password";

View File

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

View File

@ -0,0 +1,48 @@
package bisq.cli.table.column;
import java.util.stream.IntStream;
import static bisq.cli.CurrencyFormat.formatBtc;
import static com.google.common.base.Strings.padEnd;
import static java.util.Comparator.comparingInt;
public class BtcColumn extends SatoshiColumn {
public BtcColumn(String name) {
super(name);
}
@Override
public void addRow(Long value) {
rows.add(value);
String s = formatBtc(value);
stringColumn.addRow(s);
if (isNewMaxWidth.test(s))
maxWidth = s.length();
}
@Override
public String getRowAsFormattedString(int rowIndex) {
return formatBtc(getRow(rowIndex));
}
@Override
public StringColumn asStringColumn() {
// We cached the formatted satoshi strings, but we did
// not know how much zero padding each string needed until now.
int maxColumnValueWidth = stringColumn.getRows().stream()
.max(comparingInt(String::length))
.get()
.length();
IntStream.range(0, stringColumn.getRows().size()).forEach(rowIndex -> {
String btcString = stringColumn.getRow(rowIndex);
if (btcString.length() < maxColumnValueWidth) {
String paddedBtcString = padEnd(btcString, maxColumnValueWidth, '0');
stringColumn.updateRow(rowIndex, paddedBtcString);
}
});
return stringColumn.justify();
}
}

View File

@ -0,0 +1,122 @@
/*
* 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.List;
public interface Column<T> {
enum JUSTIFICATION {
LEFT,
RIGHT,
NONE
}
/**
* Returns the column's name.
*
* @return name as String
*/
String getName();
/**
* Sets the column name.
*
* @param name of the column
*/
void setName(String name);
/**
* Add column value.
*
* @param value added to column's data (row)
*/
void addRow(T value);
/**
* Returns the column data.
*
* @return rows as List<T>
*/
List<T> getRows();
/**
* Returns the maximum width of the column name, or longest,
* formatted string value -- whichever is greater.
*
* @return width of the populated column as int
*/
int getWidth();
/**
* Returns the number of rows in the column.
*
* @return number of rows in the column as int.
*/
int rowCount();
/**
* Returns true if the column has no data.
*
* @return true if empty, false if not
*/
boolean isEmpty();
/**
* Returns the column value (data) at given row index.
*
* @return value object
*/
T getRow(int rowIndex);
/**
* Update an existing value at the given row index to a new value.
*
* @param rowIndex row index of value to be updated
* @param newValue new value
*/
void updateRow(int rowIndex, T newValue);
/**
* Returns the row value as a formatted String.
*
* @return a row value as formatted String
*/
String getRowAsFormattedString(int rowIndex);
/**
* Return the column with all of its data as a StringColumn with all of its
* formatted string data.
*
* @return StringColumn
*/
StringColumn asStringColumn();
/**
* Convenience for justifying populated StringColumns before being displayed.
* Is only useful for StringColumn instances.
*/
Column<T> justify();
/**
* Returns JUSTIFICATION value (RIGHT|LEFT|NONE) for the column.
*
* @return column JUSTIFICATION
*/
JUSTIFICATION getJustification();
}

View File

@ -0,0 +1,93 @@
/*
* 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.RIGHT;
/**
* For displaying Double values.
*/
public class DoubleColumn extends NumberColumn<DoubleColumn, Double> {
protected final List<Double> rows = new ArrayList<>();
protected final Predicate<String> isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth;
// The default DoubleColumn JUSTIFICATION is RIGHT.
public DoubleColumn(String name) {
this(name, RIGHT);
}
public DoubleColumn(String name, JUSTIFICATION justification) {
super(name, justification);
this.maxWidth = name.length();
}
@Override
public void addRow(Double value) {
rows.add(value);
String s = String.valueOf(value);
if (isNewMaxWidth.test(s))
maxWidth = s.length();
}
@Override
public List<Double> getRows() {
return rows;
}
@Override
public int rowCount() {
return rows.size();
}
@Override
public boolean isEmpty() {
return rows.isEmpty();
}
@Override
public Double getRow(int rowIndex) {
return rows.get(rowIndex);
}
@Override
public void updateRow(int rowIndex, Double newValue) {
rows.set(rowIndex, newValue);
}
@Override
public String getRowAsFormattedString(int rowIndex) {
String s = String.valueOf(getRow(rowIndex));
return toJustifiedString(s);
}
@Override
public StringColumn asStringColumn() {
IntStream.range(0, rows.size()).forEachOrdered(rowIndex ->
stringColumn.addRow(getRowAsFormattedString(rowIndex)));
return stringColumn;
}
}

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