diff --git a/apitest/docs/api-beta-test-guide.md b/apitest/docs/api-beta-test-guide.md index 89fe788c58..00b0dc9551 100644 --- a/apitest/docs/api-beta-test-guide.md +++ b/apitest/docs/api-beta-test-guide.md @@ -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= --cur To look at a specific offer you created: ``` -$ ./bisq-cli --password=xyz --port=9998 getmyoffer --offer-id= +$ ./bisq-cli --password=xyz --port=9998 getoffer --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= --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 trade’s 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 trade’s state `CLOSED`. Please close out your trades with one or the other command. Each of the CLI commands above takes one argument: `--trade-id=`: ``` $ ./bisq-cli --password=xyz --port=9998 confirmpaymentstarted --trade-id= $ ./bisq-cli --password=xyz --port=9999 confirmpaymentreceived --trade-id= -$ ./bisq-cli --password=xyz --port=9998 keepfunds --trade-id= +$ ./bisq-cli --password=xyz --port=9998 closetrade --trade-id= $ ./bisq-cli --password=xyz --port=9999 withdrawfunds --trade-id= --address= [--memo=<"memo">] ``` diff --git a/apitest/docs/build-run.md b/apitest/docs/build-run.md index 6d3a0656b9..a11fe2ab77 100644 --- a/apitest/docs/build-run.md +++ b/apitest/docs/build-run.md @@ -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 diff --git a/apitest/scripts/limit-order-simulation.sh b/apitest/scripts/limit-order-simulation.sh index 2f7b1479bb..d47404f6df 100755 --- a/apitest/scripts/limit-order-simulation.sh +++ b/apitest/scripts/limit-order-simulation.sh @@ -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 $? diff --git a/apitest/scripts/rolling-offer-simulation.sh b/apitest/scripts/rolling-offer-simulation.sh index c269940e64..48fc431f63 100755 --- a/apitest/scripts/rolling-offer-simulation.sh +++ b/apitest/scripts/rolling-offer-simulation.sh @@ -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 $? diff --git a/apitest/scripts/trade-simulation-utils.sh b/apitest/scripts/trade-simulation-utils.sh index 406d4fa8f6..577cc1fff3 100755 --- a/apitest/scripts/trade-simulation-utils.sh +++ b/apitest/scripts/trade-simulation-utils.sh @@ -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" - 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 + # 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" 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 diff --git a/apitest/scripts/trade-simulation.sh b/apitest/scripts/trade-simulation.sh index 5aa540bf71..5be7659cd4 100755 --- a/apitest/scripts/trade-simulation.sh +++ b/apitest/scripts/trade-simulation.sh @@ -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 -c -m - f -a +# Script options: -d -c -m -f -a # # 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 $? diff --git a/apitest/scripts/trade-xmr-simulation.sh b/apitest/scripts/trade-xmr-simulation.sh new file mode 100755 index 0000000000..e361353da9 --- /dev/null +++ b/apitest/scripts/trade-xmr-simulation.sh @@ -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 -m -f -a +# +# 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 diff --git a/apitest/src/main/java/bisq/apitest/ApiTestMain.java b/apitest/src/main/java/bisq/apitest/ApiTestMain.java index 894decbc08..f627e96a1c 100644 --- a/apitest/src/main/java/bisq/apitest/ApiTestMain.java +++ b/apitest/src/main/java/bisq/apitest/ApiTestMain.java @@ -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) { - new ApiTestMain().execute(args); + if (!hasCallRateMeteringConfigPathOpt(args)) + new ApiTestMain().execute(getAppendedArgs(args)); + else + new ApiTestMain().execute(args); } - public void execute(@SuppressWarnings("unused") String[] args) { + public void execute(String[] args) { try { + 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); + } } diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index 8455a585b2..39c6275338 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -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() diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index f3bfde8005..0c7675d17c 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -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"; @@ -149,7 +151,7 @@ public class ApiTestConfig { ArgumentAcceptingOptionSpec configFileOpt = parser.accepts(CONFIG_FILE, format("Specify configuration file. " + - "Relative paths will be prefixed by %s location.", userDir)) + "Relative paths will be prefixed by %s location.", userDir)) .withRequiredArg() .ofType(String.class) .defaultsTo(DEFAULT_CONFIG_FILE_NAME); @@ -206,55 +208,55 @@ public class ApiTestConfig { ArgumentAcceptingOptionSpec runSubprojectJarsOpt = parser.accepts(RUN_SUBPROJECT_JARS, - "Run subproject build jars instead of full build jars") + "Run subproject build jars instead of full build jars") .withRequiredArg() .ofType(Boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec bisqAppInitTimeOpt = parser.accepts(BISQ_APP_INIT_TIME, - "Amount of time (ms) to wait on a Bisq instance's initialization") + "Amount of time (ms) to wait on a Bisq instance's initialization") .withRequiredArg() .ofType(Long.class) .defaultsTo(5000L); ArgumentAcceptingOptionSpec skipTestsOpt = parser.accepts(SKIP_TESTS, - "Start apps, but skip tests") + "Start apps, but skip tests") .withRequiredArg() .ofType(Boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec shutdownAfterTestsOpt = parser.accepts(SHUTDOWN_AFTER_TESTS, - "Terminate all processes after tests") + "Terminate all processes after tests") .withRequiredArg() .ofType(Boolean.class) .defaultsTo(true); ArgumentAcceptingOptionSpec supportingAppsOpt = parser.accepts(SUPPORTING_APPS, - "Comma delimited list of supporting apps (bitcoind,seednode,arbdaemon,...") + "Comma delimited list of supporting apps (bitcoind,seednode,arbdaemon,...") .withRequiredArg() .ofType(String.class) .defaultsTo("bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon"); ArgumentAcceptingOptionSpec callRateMeteringConfigPathOpt = parser.accepts(CALL_RATE_METERING_CONFIG_PATH, - "Install a ratemeters.json file to configure call rate metering interceptors") + "Install a ratemeters.json file to configure call rate metering interceptors") .withRequiredArg() .defaultsTo(EMPTY); ArgumentAcceptingOptionSpec enableBisqDebuggingOpt = parser.accepts(ENABLE_BISQ_DEBUGGING, - "Start Bisq apps with remote debug options") + "Start Bisq apps with remote debug options") .withRequiredArg() .ofType(Boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec registerDisputeAgentsOpt = parser.accepts(REGISTER_DISPUTE_AGENTS, - "Register dispute agents in arbitration daemon") + "Register dispute agents in arbitration daemon") .withRequiredArg() .ofType(Boolean.class) .defaultsTo(true); diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestRateMeterInterceptorConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestRateMeterInterceptorConfig.java new file mode 100644 index 0000000000..9655f21dbc --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestRateMeterInterceptorConfig.java @@ -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; + } + } +} diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java index ed759afa9a..f8be98ce54 100644 --- a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java @@ -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())); diff --git a/apitest/src/main/java/bisq/apitest/linux/BashCommand.java b/apitest/src/main/java/bisq/apitest/linux/BashCommand.java index 7db542f8e1..4c3f6b270a 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BashCommand.java +++ b/apitest/src/main/java/bisq/apitest/linux/BashCommand.java @@ -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 + @Nullable public String getOutput() { return this.output; } @@ -101,7 +104,6 @@ public class BashCommand { return this.error; } - @NotNull private List tokenizeSystemCommand() { return new ArrayList<>() {{ add(BASH_PATH_VALUE); diff --git a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java index d43e042ee3..597b59cb81 100644 --- a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java +++ b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java @@ -57,15 +57,13 @@ class SystemCommandExecutor { private ThreadedStreamHandler errorStreamHandler; public SystemCommandExecutor(final List 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; } diff --git a/apitest/src/main/resources/haveno.properties b/apitest/src/main/resources/haveno.properties new file mode 100644 index 0000000000..8c4b02d4fe --- /dev/null +++ b/apitest/src/main/resources/haveno.properties @@ -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 diff --git a/apitest/src/test/java/bisq/apitest/ApiTestCase.java b/apitest/src/test/java/bisq/apitest/ApiTestCase.java index 39e8bc0c3c..6a1bbc6c2d 100644 --- a/apitest/src/test/java/bisq/apitest/ApiTestCase.java +++ b/apitest/src/test/java/bisq/apitest/ApiTestCase.java @@ -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; - } } diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index 78f1fd0aff..fa1d09f937 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -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[], 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 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()); + } } diff --git a/apitest/src/test/java/bisq/apitest/method/RegisterDisputeAgentsTest.java b/apitest/src/test/java/bisq/apitest/method/RegisterDisputeAgentsTest.java index ad106a72a2..71e012e3fa 100644 --- a/apitest/src/test/java/bisq/apitest/method/RegisterDisputeAgentsTest.java +++ b/apitest/src/test/java/bisq/apitest/method/RegisterDisputeAgentsTest.java @@ -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()); } diff --git a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java index c37ac599b3..dbc11f9bdb 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java @@ -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 toOfferTable = (offer) -> + new TableBuilder(OFFER_TBL, offer).build().toString(); + + protected static final Function, 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}); + } } diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java index a64953d6d6..72a3dadeea 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java @@ -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 diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java index 71e654d7a4..8d751c0fb7 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java @@ -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()); } } diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java index 71a5a9a2ff..0505492e5a 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -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; diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateXMROffersTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateXMROffersTest.java new file mode 100644 index 0000000000..e4dddb7a43 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateXMROffersTest.java @@ -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 . + */ + +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 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 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); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java index b5af15bca3..1409da2879 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java @@ -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()); diff --git a/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java index 433898731e..d1aa90b3ad 100644 --- a/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java @@ -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); - assertEquals(PROPERTY_VALUE_JSON_COMMENTS, emptyForm.get(PROPERTY_NAME_JSON_COMMENTS)); + + if (paymentMethodId.equals("SWIFT_ID")) { + assertEquals(getSwiftFormComments(), emptyForm.get(PROPERTY_NAME_JSON_COMMENTS)); + } else { + assertEquals(PROPERTY_VALUE_JSON_COMMENTS, emptyForm.get(PROPERTY_NAME_JSON_COMMENTS)); + } + assertEquals(paymentMethodId, emptyForm.get(PROPERTY_NAME_PAYMENT_METHOD_ID)); assertEquals("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 expectedFiatCurrencies, + PaymentAccount paymentAccount) { + assertNotNull(paymentAccount.getTradeCurrencies()); + List expectedTradeCurrencies = new ArrayList<>() {{ + addAll(expectedFiatCurrencies); + }}; + assertArrayEquals(expectedTradeCurrencies.toArray(), paymentAccount.getTradeCurrencies().toArray()); + } + protected final void verifyAccountTradeCurrencies(List 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 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 fiatCurrencies) { + return fiatCurrencies.stream() + .sorted(Comparator.comparing(TradeCurrency::getCode)) + .map(c -> c.getCurrency().getCurrencyCode()) + .collect(Collectors.joining(",")); + } + + protected final List getSwiftFormComments() { + List comments = new ArrayList<>(); + comments.addAll(PROPERTY_VALUE_JSON_COMMENTS); + List wrappedSwiftComments = Res.getWrappedAsList("payment.swift.info.account", 110); + comments.addAll(wrappedSwiftComments); + return comments; + } + + private File fillPaymentAccountForm(List 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(); diff --git a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java index 5b373382b8..6bfffaa0d4 100644 --- a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java @@ -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 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 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()); } } } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java index 6331c339f1..e3045aa41b 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java @@ -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 maxTradeStateAndPhaseChecks = () -> isLongRunningTest ? 10 : 2; + protected final Function toTradeDetailTable = (trade) -> + new TableBuilder(TRADE_DETAIL_TBL, trade).build().toString(); + protected final Function 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 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 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); - } - - protected final void logTrade(Logger log, - TestInfo testInfo, - String description, - TradeInfo trade, - boolean force) { - if (force) - log.info(String.format("%s %s%n%s", + if (log.isDebugEnabled()) { + log.debug(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))); + description, + new TableBuilder(TRADE_DETAIL_TBL, trade).build())); } } + + 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"}); + } } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/ExpectedProtocolStatus.java b/apitest/src/test/java/bisq/apitest/method/trade/ExpectedProtocolStatus.java index 4256cb21d7..3c58b3ae90 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/ExpectedProtocolStatus.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/ExpectedProtocolStatus.java @@ -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; } } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java index 49b1a9de0b..c8a9957990 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -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 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 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); } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java new file mode 100644 index 0000000000..e15cf248eb --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java @@ -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 . + */ + +/* + * 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 . + */ + +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())); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyXMROfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyXMROfferTest.java new file mode 100644 index 0000000000..73df80550c --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyXMROfferTest.java @@ -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 . + */ + +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); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java index 54837b27b0..5f9aecb4a9 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -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 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 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); - } - } } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellXMROfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellXMROfferTest.java new file mode 100644 index 0000000000..03582392d8 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellXMROfferTest.java @@ -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 . + */ + +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); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BtcTxFeeRateTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BtcTxFeeRateTest.java index d74dff5de4..611b45ab17 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/BtcTxFeeRateTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BtcTxFeeRateTest.java @@ -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()); } diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java index 635ead3b85..53de390505 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java @@ -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]. diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java index 3934d52816..f600480b94 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java @@ -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 diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/WalletTestUtil.java b/apitest/src/test/java/bisq/apitest/method/wallet/WalletTestUtil.java index 3c2da836dc..915607bc25 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/WalletTestUtil.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/WalletTestUtil.java @@ -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()); diff --git a/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java b/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java new file mode 100644 index 0000000000..59fbc27bba --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java @@ -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 . + */ + +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; + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/LongRunningTradesTest.java b/apitest/src/test/java/bisq/apitest/scenario/LongRunningTradesTest.java index d31cedf41b..b43d0e32bc 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/LongRunningTradesTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/LongRunningTradesTest.java @@ -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() { diff --git a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java index d3c510ce4d..5e078455d6 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java @@ -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(); } } diff --git a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java index 8711e7a43e..47d0eb166d 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java @@ -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); diff --git a/apitest/src/test/java/bisq/apitest/scenario/StartupTest.java b/apitest/src/test/java/bisq/apitest/scenario/StartupTest.java index 9b97b868ab..4661dec93a 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/StartupTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/StartupTest.java @@ -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, diff --git a/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java index cdf3568400..1cd60b69ba 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java @@ -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); } } diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/AbstractBotTest.java b/apitest/src/test/java/bisq/apitest/scenario/bot/AbstractBotTest.java index afde40bb5c..0d9e627c93 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/AbstractBotTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/AbstractBotTest.java @@ -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(); diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/Bot.java b/apitest/src/test/java/bisq/apitest/scenario/bot/Bot.java index 2e8a248a4c..1fb46e717f 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/Bot.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/Bot.java @@ -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()); diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java b/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java index c5ad476cbd..046f3aaae0 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java @@ -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 diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java b/apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java index bd30c4292f..b5cd60a7dc 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java @@ -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); } } diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/RobotBob.java b/apitest/src/test/java/bisq/apitest/scenario/bot/RobotBob.java index 1f1bec8e96..5b9982964c 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/RobotBob.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/RobotBob.java @@ -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 { diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/BotProtocol.java b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/BotProtocol.java index 9add65823a..566e687394 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/BotProtocol.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/BotProtocol.java @@ -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 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; diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/MakerBotProtocol.java b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/MakerBotProtocol.java index 0ce26002ec..533d5f1c99 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/MakerBotProtocol.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/MakerBotProtocol.java @@ -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 closeTrade = waitForPayoutTx.andThen(keepFundsFromTrade); - closeTrade.apply(trade); - currentProtocolStep = DONE; } private final Supplier 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 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. diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/ProtocolStep.java b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/ProtocolStep.java index 52d5235df9..2c8c8cd07f 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/ProtocolStep.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/ProtocolStep.java @@ -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 } diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/TakerBotProtocol.java b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/TakerBotProtocol.java index a7b3117226..a8635056f1 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/TakerBotProtocol.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/TakerBotProtocol.java @@ -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 closeTrade = waitForPayoutTx.andThen(keepFundsFromTrade); - closeTrade.apply(trade); - currentProtocolStep = DONE; } private final Supplier> 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"); } diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/script/BashScriptGenerator.java b/apitest/src/test/java/bisq/apitest/scenario/bot/script/BashScriptGenerator.java index 59a7fc6b15..55115dbb5a 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/script/BashScriptGenerator.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/script/BashScriptGenerator.java @@ -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, diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/script/BotScriptGenerator.java b/apitest/src/test/java/bisq/apitest/scenario/bot/script/BotScriptGenerator.java index 9f7d2e7ec0..501f619cca 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/script/BotScriptGenerator.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/script/BotScriptGenerator.java @@ -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, diff --git a/apitest/src/test/resources/logback.xml b/apitest/src/test/resources/logback.xml new file mode 100644 index 0000000000..28279faa11 --- /dev/null +++ b/apitest/src/test/resources/logback.xml @@ -0,0 +1,20 @@ + + + + + + %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) + + + + + + + + + diff --git a/build.gradle b/build.gradle index 142ac70dc0..2cb17d430c 100644 --- a/build.gradle +++ b/build.gradle @@ -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" } diff --git a/cli/package/create-cli-dist.sh b/cli/package/create-cli-dist.sh new file mode 100755 index 0000000000..012839342d --- /dev/null +++ b/cli/package/create-cli-dist.sh @@ -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 + diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 9ab3bc9231..bf5f38d880 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -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,15 +170,14 @@ public class CliMain { var balances = client.getBalances(currencyCode); switch (currencyCode.toUpperCase()) { case "BTC": - out.println(formatBtcBalanceInfoTbl(balances.getBtc())); + new TableBuilder(BTC_BALANCE_TBL, balances.getBtc()).build().print(out); break; - case "XMR": - out.println(formatXmrBalanceInfoTbl(balances.getXmr())); - break; case "": - default: - out.println(formatBalancesTbls(balances)); + default: { + out.println("BTC"); + new TableBuilder(BTC_BALANCE_TBL, balances.getBtc()).build().print(out); break; + } } return; } @@ -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=]", "Get server wallet balances"); + stream.format(rowFormat, getbalance.name(), "[--currency-code=]", "Get server wallet balances"); stream.println(); stream.format(rowFormat, getaddressbalance.name(), "--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=]", ""); + stream.println(); stream.format(rowFormat, sendbtc.name(), "--address= --amount= \\", "Send BTC"); stream.format(rowFormat, "", "[--tx-fee-rate=]", ""); 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=", "Set custom tx fee rate in sats/byte"); @@ -692,9 +712,17 @@ public class CliMain { stream.format(rowFormat, "", "--currency-code= \\", ""); stream.format(rowFormat, "", "--amount= \\", ""); stream.format(rowFormat, "", "[--min-amount=] \\", ""); - stream.format(rowFormat, "", "--fixed-price= | --market-price=margin= \\", ""); + stream.format(rowFormat, "", "--fixed-price= | --market-price-margin= \\", ""); stream.format(rowFormat, "", "--security-deposit= \\", ""); - stream.format(rowFormat, "", "[--fee-currency=]", ""); + stream.format(rowFormat, "", "[--fee-currency=]", ""); + stream.format(rowFormat, "", "[--trigger-price=]", ""); + stream.format(rowFormat, "", "[--swap=]", ""); + stream.println(); + stream.format(rowFormat, editoffer.name(), "--offer-id= \\", "Edit offer with id"); + stream.format(rowFormat, "", "[--fixed-price=] \\", ""); + stream.format(rowFormat, "", "[--market-price-margin=] \\", ""); + stream.format(rowFormat, "", "[--trigger-price=] \\", ""); + stream.format(rowFormat, "", "[--enabled=]", ""); stream.println(); stream.format(rowFormat, canceloffer.name(), "--offer-id=", "Cancel offer with id"); stream.println(); @@ -709,22 +737,28 @@ public class CliMain { stream.format(rowFormat, "", "--currency-code=", ""); stream.println(); stream.format(rowFormat, takeoffer.name(), "--offer-id= \\", "Take offer with id"); - stream.format(rowFormat, "", "--payment-account=", ""); - stream.format(rowFormat, "", "[--fee-currency=]", ""); + stream.format(rowFormat, "", "[--payment-account=]", ""); + stream.format(rowFormat, "", "[--fee-currency=]", ""); stream.println(); stream.format(rowFormat, gettrade.name(), "--trade-id= \\", "Get trade summary or full contract"); stream.format(rowFormat, "", "[--show-contract=]", ""); stream.println(); + stream.format(rowFormat, gettrades.name(), "[--category=]", "Get open (default), closed, or failed trades"); + stream.println(); stream.format(rowFormat, confirmpaymentstarted.name(), "--trade-id=", "Confirm payment started"); stream.println(); stream.format(rowFormat, confirmpaymentreceived.name(), "--trade-id=", "Confirm payment received"); stream.println(); - stream.format(rowFormat, keepfunds.name(), "--trade-id=", "Keep received funds in Bisq wallet"); + stream.format(rowFormat, closetrade.name(), "--trade-id=", "Close completed trade"); stream.println(); stream.format(rowFormat, withdrawfunds.name(), "--trade-id= --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=", "Change open trade to failed trade"); + stream.println(); + stream.format(rowFormat, unfailtrade.name(), "--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=", "Get a new payment account form"); @@ -732,8 +766,8 @@ public class CliMain { stream.format(rowFormat, createpaymentacct.name(), "--payment-account-form=", "Create a new payment account"); stream.println(); stream.format(rowFormat, createcryptopaymentacct.name(), "--account-name= \\", "Create a new cryptocurrency payment account"); - stream.format(rowFormat, "", "--currency-code= \\", ""); - stream.format(rowFormat, "", "--address=
", ""); + stream.format(rowFormat, "", "--currency-code= \\", ""); + stream.format(rowFormat, "", "--address=", ""); stream.format(rowFormat, "", "--trade-instant=", ""); stream.println(); stream.format(rowFormat, getpaymentaccts.name(), "", "Get user payment accounts"); diff --git a/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java b/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java index 08689a8db3..c5439a3c62 100644 --- a/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java +++ b/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java @@ -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 getSupportedCryptoCurrencies() { final List result = new ArrayList<>(); + result.add("BCH"); result.sort(String::compareTo); return result; } diff --git a/cli/src/main/java/bisq/cli/CurrencyFormat.java b/cli/src/main/java/bisq/cli/CurrencyFormat.java index 9cd3ce7ac7..8ad9ac4f3a 100644 --- a/cli/src/main/java/bisq/cli/CurrencyFormat.java +++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java @@ -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)); } diff --git a/cli/src/main/java/bisq/cli/DirectionFormat.java b/cli/src/main/java/bisq/cli/DirectionFormat.java index bdc1fd507a..392fa633b6 100644 --- a/cli/src/main/java/bisq/cli/DirectionFormat.java +++ b/cli/src/main/java/bisq/cli/DirectionFormat.java @@ -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 { diff --git a/cli/src/main/java/bisq/cli/GrpcClient.java b/cli/src/main/java/bisq/cli/GrpcClient.java index 20b63740ce..cb1d3919b9 100644 --- a/cli/src/main/java/bisq/cli/GrpcClient.java +++ b/cli/src/main/java/bisq/cli/GrpcClient.java @@ -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 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 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 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 getOffersSortedByDate(String currencyCode) { - ArrayList offers = new ArrayList<>(); - offers.addAll(getOffers(BUY.name(), currencyCode)); - offers.addAll(getOffers(SELL.name(), currencyCode)); - return sortOffersByDate(offers); + return offersServiceRequest.getOffersSortedByDate(currencyCode); } public List getOffersSortedByDate(String direction, String currencyCode) { - var offers = getOffers(direction, currencyCode); - return offers.isEmpty() ? offers : sortOffersByDate(offers); + return offersServiceRequest.getOffersSortedByDate(direction, currencyCode); } public List 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 getMyCryptoCurrencyOffers(String direction, String currencyCode) { - return getMyOffers(direction, "BTC").stream() - .filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode)) - .collect(toList()); - } - - public List getMyOffersSortedByDate(String direction, String currencyCode) { - var offers = getMyOffers(direction, currencyCode); - return offers.isEmpty() ? offers : sortOffersByDate(offers); + return offersServiceRequest.getMyOffers(direction, currencyCode); } public List getMyOffersSortedByDate(String currencyCode) { - ArrayList 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 offers = getOffersSortedByDate(direction, currencyCode); - return offers.isEmpty() ? null : offers.get(offers.size() - 1); - } - - public List sortOffersByDate(List 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 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 getTrades() { - var request = GetTradesRequest.newBuilder().build(); - return grpcStubs.tradesService.getTrades(request).getTradesList(); + public List getOpenTrades() { + return tradesServiceRequest.getOpenTrades(); + } + + public List 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 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 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 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) { diff --git a/cli/src/main/java/bisq/cli/Method.java b/cli/src/main/java/bisq/cli/Method.java index a65bc96a05..21b7712e2d 100644 --- a/cli/src/main/java/bisq/cli/Method.java +++ b/cli/src/main/java/bisq/cli/Method.java @@ -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, diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java deleted file mode 100644 index 06c5ee206d..0000000000 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ /dev/null @@ -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 . - */ - -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) { - 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 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 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 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 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 offers) { - return getLongestColumnSize( - COL_HEADER_PAYMENT_METHOD.length(), - offers.stream() - .map(OfferInfo::getPaymentMethodShortName) - .collect(Collectors.toList())); - } - - private static int getLongestAmountColWidth(List offers) { - return getLongestColumnSize( - COL_HEADER_AMOUNT.length(), - offers.stream() - .map(o -> formatAmountRange(o.getMinAmount(), o.getAmount())) - .collect(Collectors.toList())); - } - - private static int getLongestVolumeColWidth(List 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 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 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)); - } -} diff --git a/cli/src/main/java/bisq/cli/TradeFormat.java b/cli/src/main/java/bisq/cli/TradeFormat.java deleted file mode 100644 index 478f806b6c..0000000000 --- a/cli/src/main/java/bisq/cli/TradeFormat.java +++ /dev/null @@ -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 . - */ - -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 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 makerFeeHeader = () -> !isTaker ? - COL_HEADER_TRADE_MAKER_FEE + COL_HEADER_DELIMITER - : ""; - Supplier makerFeeHeaderSpec = () -> !isTaker ? - "%" + (COL_HEADER_TRADE_MAKER_FEE.length() + 2) + "s" - : ""; - Supplier takerFeeHeader = () -> isTaker ? - COL_HEADER_TRADE_TAKER_FEE + COL_HEADER_DELIMITER - : ""; - Supplier 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 priceHeader = (t) -> - t.getOffer().getBaseCurrencyCode().equals("XMR") - ? COL_HEADER_PRICE - : COL_HEADER_PRICE_OF_ALTCOIN; - - private static final Function priceHeaderCurrencyCode = (t) -> - t.getOffer().getBaseCurrencyCode().equals("XMR") - ? t.getOffer().getCounterCurrencyCode() - : t.getOffer().getBaseCurrencyCode(); - - private static final BiFunction makerTakerFeeHeaderCurrencyCode = (t, isTaker) -> { - return "XMR"; - }; - - private static final Function paymentStatusHeaderCurrencyCode = (t) -> - t.getOffer().getBaseCurrencyCode().equals("XMR") - ? t.getOffer().getCounterCurrencyCode() - : t.getOffer().getBaseCurrencyCode(); - - private static final Function priceFormat = (t) -> - t.getOffer().getBaseCurrencyCode().equals("XMR") - ? formatPrice(t.getTradePrice()) - : formatCryptoCurrencyPrice(t.getOffer().getPrice()); - - private static final Function amountFormat = (t) -> - t.getOffer().getBaseCurrencyCode().equals("XMR") - ? formatXmr(ParsingUtils.centinerosToAtomicUnits(t.getTradeAmountAsLong())) - : formatCryptoCurrencyOfferVolume(t.getOffer().getVolume()); - - private static final BiFunction makerTakerMinerTxFeeFormat = (t, isTaker) -> { - if (isTaker) { - return formatSatoshis(t.getTxFeeAsLong()); - } else { - return formatSatoshis(t.getOffer().getTxFee()); - } - }; - - private static final BiFunction makerTakerFeeFormat = (t, isTaker) -> { - return formatXmr(ParsingUtils.centinerosToAtomicUnits(t.getTakerFeeAsLong())); - }; - - private static final Function tradeCostFormat = (t) -> - t.getOffer().getBaseCurrencyCode().equals("XMR") - ? formatOfferVolume(t.getOffer().getVolume()) - : formatXmr(ParsingUtils.centinerosToAtomicUnits(t.getTradeAmountAsLong())); - -} diff --git a/cli/src/main/java/bisq/cli/opts/AbstractMethodOptionParser.java b/cli/src/main/java/bisq/cli/opts/AbstractMethodOptionParser.java index 623fa2f2d6..5fe5016262 100644 --- a/cli/src/main/java/bisq/cli/opts/AbstractMethodOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/AbstractMethodOptionParser.java @@ -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 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) 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> valueNotSpecified = (opt) -> + !options.hasArgument(opt) || options.valueOf(opt).isEmpty(); + private final Function cliExceptionMessageStyle = (ex) -> { if (ex.getMessage() == null) return null; diff --git a/cli/src/main/java/bisq/cli/opts/CancelOfferOptionParser.java b/cli/src/main/java/bisq/cli/opts/CancelOfferOptionParser.java index 8ae56d06be..83d400469c 100644 --- a/cli/src/main/java/bisq/cli/opts/CancelOfferOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/CancelOfferOptionParser.java @@ -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 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; } diff --git a/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java b/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java index f3e5f3cc03..2972f5e590 100644 --- a/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java @@ -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 accountNameOpt = parser.accepts(OPT_ACCOUNT_NAME, "crypto currency account name") .withRequiredArg(); - final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "crypto currency code") + final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "crypto currency code (xmr)") .withRequiredArg(); - final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "address") + final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "altcoin address") .withRequiredArg(); final OptionSpec tradeInstantOpt = parser.accepts(OPT_TRADE_INSTANT, "create trade instant account") @@ -54,9 +56,19 @@ public class CreateCryptoCurrencyPaymentAcctOptionParser extends AbstractMethodO if (!options.has(accountNameOpt) || options.valueOf(accountNameOpt).isEmpty()) throw new IllegalArgumentException("no payment account name specified"); - + 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; } diff --git a/cli/src/main/java/bisq/cli/opts/CreateOfferOptionParser.java b/cli/src/main/java/bisq/cli/opts/CreateOfferOptionParser.java index 514a0f438d..9d8345264f 100644 --- a/cli/src/main/java/bisq/cli/opts/CreateOfferOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/CreateOfferOptionParser.java @@ -20,22 +20,20 @@ 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 paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT, - "id of payment account used for offer") + final OptionSpec paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT_ID, + "id of payment account used for offer") .withRequiredArg() .defaultsTo(EMPTY); final OptionSpec directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)") .withRequiredArg(); - final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (eur|usd|...)") + final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (xmr|eur|usd|...)") .withRequiredArg(); final OptionSpec amountOpt = parser.accepts(OPT_AMOUNT, "amount of btc to buy or sell") @@ -44,7 +42,7 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implemen final OptionSpec minAmountOpt = parser.accepts(OPT_MIN_AMOUNT, "minimum amount of btc to buy or sell") .withOptionalArg(); - final OptionSpec mktPriceMarginOpt = parser.accepts(OPT_MKT_PRICE_MARGIN, "market btc price margin (%)") + final OptionSpec 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 securityDepositOpt = parser.accepts(OPT_SECURITY_DEPOSIT, "maker security deposit (%)") + final OptionSpec 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()) - throw new IllegalArgumentException("no market price margin specified"); + 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)); } } diff --git a/cli/src/main/java/bisq/cli/opts/CreatePaymentAcctOptionParser.java b/cli/src/main/java/bisq/cli/opts/CreatePaymentAcctOptionParser.java index e324ee307c..74703e55a3 100644 --- a/cli/src/main/java/bisq/cli/opts/CreatePaymentAcctOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/CreatePaymentAcctOptionParser.java @@ -29,7 +29,7 @@ import static java.lang.String.format; public class CreatePaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec paymentAcctFormPathOpt = parser.accepts(OPT_PAYMENT_ACCOUNT_FORM, - "path to json payment account form") + "path to json payment account form") .withRequiredArg(); public CreatePaymentAcctOptionParser(String[] args) { diff --git a/cli/src/main/java/bisq/cli/opts/GetOffersOptionParser.java b/cli/src/main/java/bisq/cli/opts/GetOffersOptionParser.java index 32c44a2163..a3f0e5b360 100644 --- a/cli/src/main/java/bisq/cli/opts/GetOffersOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/GetOffersOptionParser.java @@ -28,7 +28,7 @@ public class GetOffersOptionParser extends AbstractMethodOptionParser implements final OptionSpec directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)") .withRequiredArg(); - final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (eur|usd|...)") + final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (xmr|eur|usd|...)") .withRequiredArg(); public GetOffersOptionParser(String[] args) { diff --git a/cli/src/main/java/bisq/cli/opts/GetPaymentAcctFormOptionParser.java b/cli/src/main/java/bisq/cli/opts/GetPaymentAcctFormOptionParser.java index baf703d7e2..adc33bbbf1 100644 --- a/cli/src/main/java/bisq/cli/opts/GetPaymentAcctFormOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/GetPaymentAcctFormOptionParser.java @@ -25,7 +25,7 @@ import static bisq.cli.opts.OptLabel.OPT_PAYMENT_METHOD_ID; public class GetPaymentAcctFormOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec paymentMethodIdOpt = parser.accepts(OPT_PAYMENT_METHOD_ID, - "id of payment method type used by a payment account") + "id of payment method type used by a payment account") .withRequiredArg(); public GetPaymentAcctFormOptionParser(String[] args) { diff --git a/cli/src/main/java/bisq/cli/opts/GetTradesOptionParser.java b/cli/src/main/java/bisq/cli/opts/GetTradesOptionParser.java new file mode 100644 index 0000000000..8357db655d --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/GetTradesOptionParser.java @@ -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 . + */ + +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 categoryOpt = parser.accepts(OPT_CATEGORY, + "category of trades (open|closed|failed)") + .withRequiredArg() + .defaultsTo(CATEGORY.open.name()); + + private final Predicate 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; + } +} diff --git a/cli/src/main/java/bisq/cli/opts/OfferIdOptionParser.java b/cli/src/main/java/bisq/cli/opts/OfferIdOptionParser.java new file mode 100644 index 0000000000..cd20f30e01 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/OfferIdOptionParser.java @@ -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 . + */ + +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 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); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/OptLabel.java b/cli/src/main/java/bisq/cli/opts/OptLabel.java index 742cb9dddc..5ccc135c08 100644 --- a/cli/src/main/java/bisq/cli/opts/OptLabel.java +++ b/cli/src/main/java/bisq/cli/opts/OptLabel.java @@ -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"; diff --git a/cli/src/main/java/bisq/cli/opts/SetTxFeeRateOptionParser.java b/cli/src/main/java/bisq/cli/opts/SetTxFeeRateOptionParser.java index adb01fa182..3bc0a51b7e 100644 --- a/cli/src/main/java/bisq/cli/opts/SetTxFeeRateOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/SetTxFeeRateOptionParser.java @@ -25,7 +25,7 @@ import static bisq.cli.opts.OptLabel.OPT_TX_FEE_RATE; public class SetTxFeeRateOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec feeRateOpt = parser.accepts(OPT_TX_FEE_RATE, - "tx fee rate preference (sats/byte)") + "tx fee rate preference (sats/byte)") .withRequiredArg(); public SetTxFeeRateOptionParser(String[] args) { diff --git a/cli/src/main/java/bisq/cli/opts/TakeOfferOptionParser.java b/cli/src/main/java/bisq/cli/opts/TakeOfferOptionParser.java index a2eadae7e7..d67305f544 100644 --- a/cli/src/main/java/bisq/cli/opts/TakeOfferOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/TakeOfferOptionParser.java @@ -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 offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to take") - .withRequiredArg(); - - final OptionSpec paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT, "id of payment account used for trade") + final OptionSpec 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); } diff --git a/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java b/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java new file mode 100644 index 0000000000..9526bee4dc --- /dev/null +++ b/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java @@ -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 . + */ + +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 getOffers(String direction, String currencyCode) { + var request = GetOffersRequest.newBuilder() + .setDirection(direction) + .setCurrencyCode(currencyCode) + .build(); + return grpcStubs.offersService.getOffers(request).getOffersList(); + } + + public List getOffersSortedByDate(String currencyCode) { + ArrayList offers = new ArrayList<>(); + offers.addAll(getOffers(BUY.name(), currencyCode)); + offers.addAll(getOffers(SELL.name(), currencyCode)); + return offers.isEmpty() ? offers : sortOffersByDate(offers); + } + + public List getOffersSortedByDate(String direction, String currencyCode) { + var offers = getOffers(direction, currencyCode); + return offers.isEmpty() ? offers : sortOffersByDate(offers); + } + + public List getMyOffers(String direction, String currencyCode) { + var request = GetMyOffersRequest.newBuilder() + .setDirection(direction) + .setCurrencyCode(currencyCode) + .build(); + return grpcStubs.offersService.getMyOffers(request).getOffersList(); + } + + public List getMyOffersSortedByDate(String currencyCode) { + ArrayList offers = new ArrayList<>(); + offers.addAll(getMyOffers(BUY.name(), currencyCode)); + offers.addAll(getMyOffers(SELL.name(), currencyCode)); + return offers.isEmpty() ? offers : sortOffersByDate(offers); + } + + public List getMyOffersSortedByDate(String direction, String currencyCode) { + var offers = getMyOffers(direction, currencyCode); + return offers.isEmpty() ? offers : sortOffersByDate(offers); + } + + public OfferInfo getMostRecentOffer(String direction, String currencyCode) { + List offers = getOffersSortedByDate(direction, currencyCode); + return offers.isEmpty() ? null : offers.get(offers.size() - 1); + } + + public List sortOffersByDate(List offerInfoList) { + return offerInfoList.stream() + .sorted(comparing(OfferInfo::getDate)) + .collect(toList()); + } +} diff --git a/cli/src/main/java/bisq/cli/request/PaymentAccountsServiceRequest.java b/cli/src/main/java/bisq/cli/request/PaymentAccountsServiceRequest.java new file mode 100644 index 0000000000..709a765958 --- /dev/null +++ b/cli/src/main/java/bisq/cli/request/PaymentAccountsServiceRequest.java @@ -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 . + */ + +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 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 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 getCryptoPaymentMethods() { + var request = GetCryptoCurrencyPaymentMethodsRequest.newBuilder().build(); + return grpcStubs.paymentAccountsService.getCryptoCurrencyPaymentMethods(request).getPaymentMethodsList(); + } +} diff --git a/cli/src/main/java/bisq/cli/request/TradesServiceRequest.java b/cli/src/main/java/bisq/cli/request/TradesServiceRequest.java new file mode 100644 index 0000000000..6819e919cf --- /dev/null +++ b/cli/src/main/java/bisq/cli/request/TradesServiceRequest.java @@ -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 . + */ + +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 getOpenTrades() { + var request = GetTradesRequest.newBuilder() + .build(); + return grpcStubs.tradesService.getTrades(request).getTradesList(); + } + + public List 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); + } +} diff --git a/cli/src/main/java/bisq/cli/request/WalletsServiceRequest.java b/cli/src/main/java/bisq/cli/request/WalletsServiceRequest.java new file mode 100644 index 0000000000..13ed58f3df --- /dev/null +++ b/cli/src/main/java/bisq/cli/request/WalletsServiceRequest.java @@ -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 . + */ + +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 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); + } +} diff --git a/cli/src/main/java/bisq/cli/table/Table.java b/cli/src/main/java/bisq/cli/table/Table.java new file mode 100644 index 0000000000..52c4f40c66 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/Table.java @@ -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 . + */ + +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())); + }); + } +} diff --git a/cli/src/main/java/bisq/cli/table/builder/AbstractTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/AbstractTableBuilder.java new file mode 100644 index 0000000000..46f56ca027 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/AbstractTableBuilder.java @@ -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 . + */ + +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 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(); +} diff --git a/cli/src/main/java/bisq/cli/table/builder/AbstractTradeListBuilder.java b/cli/src/main/java/bisq/cli/table/builder/AbstractTradeListBuilder.java new file mode 100644 index 0000000000..be5b986096 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/AbstractTradeListBuilder.java @@ -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 . + */ + +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 trades; + + protected final TradeTableColumnSupplier colSupplier; + + protected final Column colTradeId; + @Nullable + protected final Column colCreateDate; + @Nullable + protected final Column colMarket; + protected final Column colPrice; + @Nullable + protected final Column colPriceDeviation; + @Nullable + protected final Column colCurrency; + @Nullable + protected final Column colAmount; + @Nullable + protected final Column colMixedAmount; + @Nullable + protected final Column colMinerTxFee; + @Nullable + protected final MixedTradeFeeColumn colMixedTradeFee; + @Nullable + protected final Column colBuyerDeposit; + @Nullable + protected final Column colSellerDeposit; + @Nullable + protected final Column colPaymentMethod; + @Nullable + protected final Column colRole; + @Nullable + protected final Column colOfferType; + @Nullable + protected final Column colClosingStatus; + + // Trade detail tbl specific columns + + @Nullable + protected final Column colIsDepositPublished; + @Nullable + protected final Column colIsDepositConfirmed; + @Nullable + protected final Column colIsPayoutPublished; + @Nullable + protected final Column colIsCompleted; + @Nullable + protected final Column colBisqTradeFee; + @Nullable + protected final Column colTradeCost; + @Nullable + protected final Column colIsPaymentStartedMessageSent; + @Nullable + protected final Column colIsPaymentReceivedMessageSent; + @Nullable + protected final Column 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 isTradeDetailTblBuilder = () -> tableType.equals(TRADE_DETAIL_TBL); + protected final Predicate isFiatTrade = (t) -> isFiatOffer.test(t.getOffer()); + protected final Predicate isMyOffer = (t) -> t.getOffer().getIsMyOffer(); + protected final Predicate isTaker = (t) -> t.getRole().toLowerCase().contains("taker"); + protected final Predicate isSellOffer = (t) -> t.getOffer().getDirection().equals(SELL.name()); + protected final Predicate 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 toAltcoinTradeVolumeAsLong = (t) -> new BigDecimal(t.getTradeVolume()).movePointRight(8).longValue(); + + protected final Function toTradeVolumeAsString = (t) -> + isFiatTrade.test(t) + ? t.getTradeVolume() + : formatSatoshis(t.getAmountAsLong()); + + protected final Function toTradeVolumeAsLong = (t) -> + isFiatTrade.test(t) + ? Long.parseLong(t.getTradeVolume()) + : toAltcoinTradeVolumeAsLong.apply(t); + + protected final Function toTradeAmount = (t) -> + isFiatTrade.test(t) + ? t.getAmountAsLong() + : toTradeVolumeAsLong.apply(t); + + protected final Function toMarket = (t) -> + t.getOffer().getBaseCurrencyCode() + "/" + + t.getOffer().getCounterCurrencyCode(); + + protected final Function toPaymentCurrencyCode = (t) -> + isFiatTrade.test(t) + ? t.getOffer().getCounterCurrencyCode() + : t.getOffer().getBaseCurrencyCode(); + + protected final Function toPriceDeviation = (t) -> + t.getOffer().getUseMarketBasedPrice() + ? format("%.2f%s", t.getOffer().getMarketPriceMarginPct(), "%") + : "N/A"; + + protected final Function toMyMinerTxFee = (t) -> { + return isTaker.test(t) + ? t.getTxFeeAsLong() + : t.getOffer().getTxFee(); + }; + + protected final Function toTradeFeeBtc = (t) -> { + var isMyOffer = t.getOffer().getIsMyOffer(); + if (isMyOffer) { + return t.getOffer().getMakerFee(); + } else { + return t.getTakerFeeAsLong(); + } + }; + + protected final Function toMyMakerOrTakerFee = (t) -> { + return isTaker.test(t) + ? t.getTakerFeeAsLong() + : t.getOffer().getMakerFee(); + }; + + protected final Function 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 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 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 ""; + } + }; +} diff --git a/cli/src/main/java/bisq/cli/table/builder/AddressBalanceTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/AddressBalanceTableBuilder.java new file mode 100644 index 0000000000..52b771c7b3 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/AddressBalanceTableBuilder.java @@ -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 . + */ + +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 colAddress; + private final Column colAvailableBalance; + private final Column colConfirmations; + private final Column 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 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()); + } +} diff --git a/cli/src/main/java/bisq/cli/table/builder/BtcBalanceTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/BtcBalanceTableBuilder.java new file mode 100644 index 0000000000..7fe39a1495 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/BtcBalanceTableBuilder.java @@ -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 . + */ + +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 colAvailableBalance; + private final Column colReservedBalance; + private final Column colTotalAvailableBalance; + private final Column 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()); + } +} diff --git a/cli/src/main/java/bisq/cli/table/builder/ClosedTradeTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/ClosedTradeTableBuilder.java new file mode 100644 index 0000000000..b7201fdccc --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/ClosedTradeTableBuilder.java @@ -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 . + */ + +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)); + }); + } +} diff --git a/cli/src/main/java/bisq/cli/table/builder/FailedTradeTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/FailedTradeTableBuilder.java new file mode 100644 index 0000000000..10348f0ec1 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/FailedTradeTableBuilder.java @@ -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 . + */ + +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"); + }); + } +} diff --git a/cli/src/main/java/bisq/cli/table/builder/OfferTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/OfferTableBuilder.java new file mode 100644 index 0000000000..b1323332b1 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/OfferTableBuilder.java @@ -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 . + */ + +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 colOfferId = new StringColumn(COL_HEADER_UUID, LEFT); + private final Column colDirection = new StringColumn(COL_HEADER_DIRECTION, LEFT); + private final Column colAmount = new SatoshiColumn("Temp Amount", NONE); + private final Column colMinAmount = new SatoshiColumn("Temp Min Amount", NONE); + private final Column colPaymentMethod = new StringColumn(COL_HEADER_PAYMENT_METHOD, LEFT); + private final Column colCreateDate = new Iso8601DateTimeColumn(COL_HEADER_CREATION_DATE); + + OfferTableBuilder(List protos) { + super(OFFER_TBL, protos); + } + + @Override + public Table build() { + List offers = protos.stream().map(p -> (OfferInfo) p).collect(Collectors.toList()); + return isShowingFiatOffers.get() + ? buildFiatOfferTable(offers) + : buildCryptoCurrencyOfferTable(offers); + } + + @SuppressWarnings("ConstantConditions") + public Table buildFiatOfferTable(List offers) { + @Nullable + Column colEnabled = enabledColumn.get(); // Not boolean: "YES", "NO", or "PENDING" + Column colFiatPrice = new StringColumn(format(COL_HEADER_DETAILED_PRICE, fiatTradeCurrency.get()), RIGHT); + Column colVolume = new StringColumn(format("Temp Volume (%s)", fiatTradeCurrency.get()), NONE); + Column colMinVolume = new StringColumn(format("Temp Min Volume (%s)", fiatTradeCurrency.get()), NONE); + @Nullable + Column 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 offers) { + @Nullable + Column colEnabled = enabledColumn.get(); // Not boolean: YES, NO, or PENDING + Column colBtcPrice = new StringColumn(format(COL_HEADER_DETAILED_PRICE_OF_ALTCOIN, altcoinTradeCurrency.get()), RIGHT); + Column colVolume = new StringColumn(format("Temp Volume (%s)", altcoinTradeCurrency.get()), NONE); + Column colMinVolume = new StringColumn(format("Temp Min Volume (%s)", altcoinTradeCurrency.get()), NONE); + @Nullable + Column 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 toBlankOrNonZeroValue = (s) -> s.trim().equals("0") ? "" : s; + private final Supplier firstOfferInList = () -> (OfferInfo) protos.get(0); + private final Supplier isShowingMyOffers = () -> firstOfferInList.get().getIsMyOffer(); + private final Supplier isShowingFiatOffers = () -> isFiatOffer.test(firstOfferInList.get()); + private final Supplier fiatTradeCurrency = () -> firstOfferInList.get().getCounterCurrencyCode(); + private final Supplier altcoinTradeCurrency = () -> firstOfferInList.get().getBaseCurrencyCode(); + private final Supplier isShowingBsqOffers = () -> + !isFiatOffer.test(firstOfferInList.get()) && altcoinTradeCurrency.get().equals("BSQ"); + + @Nullable // Not a boolean column: YES, NO, or PENDING. + private final Supplier enabledColumn = () -> + isShowingMyOffers.get() + ? new StringColumn(COL_HEADER_ENABLED, LEFT) + : null; + @Nullable + private final Supplier fiatTriggerPriceColumn = () -> + isShowingMyOffers.get() + ? new StringColumn(format(COL_HEADER_TRIGGER_PRICE, fiatTradeCurrency.get()), RIGHT) + : null; + @Nullable + private final Supplier altcoinTriggerPriceColumn = () -> + isShowingMyOffers.get() && !isShowingBsqOffers.get() + ? new StringColumn(format(COL_HEADER_TRIGGER_PRICE, altcoinTradeCurrency.get()), RIGHT) + : null; + + private final Function toEnabled = (o) -> { + return o.getIsActivated() ? "YES" : "NO"; + }; + + private final Function toMirroredDirection = (d) -> + d.equalsIgnoreCase(BUY.name()) ? SELL.name() : BUY.name(); + + private final Function 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 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 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()); + }; +} diff --git a/cli/src/main/java/bisq/cli/table/builder/OpenTradeTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/OpenTradeTableBuilder.java new file mode 100644 index 0000000000..10770c7183 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/OpenTradeTableBuilder.java @@ -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 . + */ + +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()); + }); + } +} diff --git a/cli/src/main/java/bisq/cli/table/builder/PaymentAccountTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/PaymentAccountTableBuilder.java new file mode 100644 index 0000000000..1dd2c5b89f --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/PaymentAccountTableBuilder.java @@ -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 . + */ + +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 colName; + private final Column colCurrency; + private final Column colPaymentMethod; + private final Column 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 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); + } +} diff --git a/cli/src/main/java/bisq/cli/table/builder/TableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/TableBuilder.java new file mode 100644 index 0000000000..7bfaad2b95 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/TableBuilder.java @@ -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 . + */ + +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()); + } + } +} diff --git a/cli/src/main/java/bisq/cli/table/builder/TableBuilderConstants.java b/cli/src/main/java/bisq/cli/table/builder/TableBuilderConstants.java new file mode 100644 index 0000000000..2c687e2648 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/TableBuilderConstants.java @@ -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 . + */ + +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"; +} diff --git a/cli/src/main/java/bisq/cli/table/builder/TableType.java b/cli/src/main/java/bisq/cli/table/builder/TableType.java new file mode 100644 index 0000000000..3f30a476d4 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/TableType.java @@ -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 . + */ + +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 +} diff --git a/cli/src/main/java/bisq/cli/table/builder/TradeDetailTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/TradeDetailTableBuilder.java new file mode 100644 index 0000000000..287ed6686c --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/TradeDetailTableBuilder.java @@ -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 . + */ + +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> 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> defineColumnList(TradeInfo trade) { + return getBisqV1TradeColumnList(); + } + + private List> getBisqV1TradeColumnList() { + List> 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; + } +} diff --git a/cli/src/main/java/bisq/cli/table/builder/TradeTableColumnSupplier.java b/cli/src/main/java/bisq/cli/table/builder/TradeTableColumnSupplier.java new file mode 100644 index 0000000000..225926ac00 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/TradeTableColumnSupplier.java @@ -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 . + */ + +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 trades; + + public TradeTableColumnSupplier(TableType tableType, List trades) { + this.tableType = tableType; + this.trades = trades; + } + + private final Supplier isTradeDetailTblBuilder = () -> getTableType().equals(TRADE_DETAIL_TBL); + private final Supplier isOpenTradeTblBuilder = () -> getTableType().equals(OPEN_TRADES_TBL); + private final Supplier isClosedTradeTblBuilder = () -> getTableType().equals(CLOSED_TRADES_TBL); + private final Supplier isFailedTradeTblBuilder = () -> getTableType().equals(FAILED_TRADES_TBL); + private final Supplier firstRow = () -> getTrades().get(0); + private final Predicate isFiatOffer = (o) -> o.getBaseCurrencyCode().equals("BTC"); + private final Predicate isFiatTrade = (t) -> isFiatOffer.test(t.getOffer()); + private final Predicate isTaker = (t) -> t.getRole().toLowerCase().contains("taker"); + + final Supplier tradeIdColumn = () -> isTradeDetailTblBuilder.get() + ? new StringColumn(COL_HEADER_TRADE_SHORT_ID) + : new StringColumn(COL_HEADER_TRADE_ID); + + final Supplier createDateColumn = () -> isTradeDetailTblBuilder.get() + ? null + : new Iso8601DateTimeColumn(COL_HEADER_DATE_TIME); + + final Supplier marketColumn = () -> isTradeDetailTblBuilder.get() + ? null + : new StringColumn(COL_HEADER_MARKET); + + private final Function> 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> priceColumn = () -> isTradeDetailTblBuilder.get() + ? toDetailedPriceColumn.apply(firstRow.get()) + : new StringColumn(COL_HEADER_PRICE, RIGHT); + + final Supplier> priceDeviationColumn = () -> isTradeDetailTblBuilder.get() + ? null + : new StringColumn(COL_HEADER_DEVIATION, RIGHT); + + final Supplier currencyColumn = () -> isTradeDetailTblBuilder.get() + ? null + : new StringColumn(COL_HEADER_CURRENCY); + + private final Function> 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> amountColumn = () -> isTradeDetailTblBuilder.get() + ? toDetailedAmountColumn.apply(firstRow.get()) + : new BtcColumn(COL_HEADER_AMOUNT_IN_BTC); + + final Supplier mixedAmountColumn = () -> isTradeDetailTblBuilder.get() + ? null + : new StringColumn(COL_HEADER_AMOUNT, RIGHT); + + final Supplier> minerTxFeeColumn = () -> isTradeDetailTblBuilder.get() || isClosedTradeTblBuilder.get() + ? new SatoshiColumn(COL_HEADER_TX_FEE) + : null; + + final Supplier mixedTradeFeeColumn = () -> isTradeDetailTblBuilder.get() + ? null + : new MixedTradeFeeColumn(COL_HEADER_TRADE_FEE); + + final Supplier paymentMethodColumn = () -> isTradeDetailTblBuilder.get() || isClosedTradeTblBuilder.get() + ? null + : new StringColumn(COL_HEADER_PAYMENT_METHOD, LEFT); + + final Supplier roleColumn = () -> { + return isTradeDetailTblBuilder.get() || isOpenTradeTblBuilder.get() || isFailedTradeTblBuilder.get() + ? new StringColumn(COL_HEADER_TRADE_ROLE) + : null; + }; + + final Function> toSecurityDepositColumn = (name) -> isClosedTradeTblBuilder.get() + ? new SatoshiColumn(name) + : null; + + final Supplier offerTypeColumn = () -> isTradeDetailTblBuilder.get() + ? null + : new StringColumn(COL_HEADER_OFFER_TYPE); + + final Supplier statusDescriptionColumn = () -> isTradeDetailTblBuilder.get() + ? null + : new StringColumn(COL_HEADER_STATUS); + + private final Function> toBooleanColumn = BooleanColumn::new; + + final Supplier> depositPublishedColumn = () -> { + return isTradeDetailTblBuilder.get() + ? toBooleanColumn.apply(COL_HEADER_TRADE_DEPOSIT_PUBLISHED) + : null; + }; + + final Supplier> depositConfirmedColumn = () -> { + return isTradeDetailTblBuilder.get() + ? toBooleanColumn.apply(COL_HEADER_TRADE_DEPOSIT_CONFIRMED) + : null; + + }; + + final Supplier> payoutPublishedColumn = () -> { + return isTradeDetailTblBuilder.get() + ? toBooleanColumn.apply(COL_HEADER_TRADE_PAYOUT_PUBLISHED) + : null; + }; + + final Supplier> fundsWithdrawnColumn = () -> { + return isTradeDetailTblBuilder.get() + ? toBooleanColumn.apply(COL_HEADER_TRADE_WITHDRAWN) + : null; + }; + + final Supplier> 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 toPaymentCurrencyCode = (t) -> + isFiatTrade.test(t) + ? t.getOffer().getCounterCurrencyCode() + : t.getOffer().getBaseCurrencyCode(); + + final Supplier> 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> 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> 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 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> 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; + } + }; +} diff --git a/cli/src/main/java/bisq/cli/table/builder/TransactionTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/TransactionTableBuilder.java new file mode 100644 index 0000000000..0507586faf --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/TransactionTableBuilder.java @@ -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 . + */ + +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 colTxId; + private final Column colIsConfirmed; + private final Column colInputSum; + private final Column colOutputSum; + private final Column colTxFee; + private final Column 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 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()); + } + } +} diff --git a/cli/src/main/java/bisq/cli/table/column/AbstractColumn.java b/cli/src/main/java/bisq/cli/table/column/AbstractColumn.java new file mode 100644 index 0000000000..12d78b94e6 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/AbstractColumn.java @@ -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 . + */ + +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, T> implements Column { + + // 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 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; + } + } +} + diff --git a/cli/src/main/java/bisq/cli/table/column/AltcoinVolumeColumn.java b/cli/src/main/java/bisq/cli/table/column/AltcoinVolumeColumn.java new file mode 100644 index 0000000000..dbd49bbd6c --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/AltcoinVolumeColumn.java @@ -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 . + */ + +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 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); + } + }; +} diff --git a/cli/src/main/java/bisq/cli/table/column/BooleanColumn.java b/cli/src/main/java/bisq/cli/table/column/BooleanColumn.java new file mode 100644 index 0000000000..841fd5bf3e --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/BooleanColumn.java @@ -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 . + */ + +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 { + + private static final String DEFAULT_TRUE_AS_STRING = "YES"; + private static final String DEFAULT_FALSE_AS_STRING = "NO"; + + private final List rows = new ArrayList<>(); + + private final Predicate 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 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; + } +} diff --git a/cli/src/main/java/bisq/cli/table/column/BtcColumn.java b/cli/src/main/java/bisq/cli/table/column/BtcColumn.java new file mode 100644 index 0000000000..3d2062d8df --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/BtcColumn.java @@ -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(); + } +} diff --git a/cli/src/main/java/bisq/cli/table/column/Column.java b/cli/src/main/java/bisq/cli/table/column/Column.java new file mode 100644 index 0000000000..dc39af17e3 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/Column.java @@ -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 . + */ + +package bisq.cli.table.column; + +import java.util.List; + +public interface Column { + + 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 + */ + List 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 justify(); + + /** + * Returns JUSTIFICATION value (RIGHT|LEFT|NONE) for the column. + * + * @return column JUSTIFICATION + */ + JUSTIFICATION getJustification(); +} diff --git a/cli/src/main/java/bisq/cli/table/column/DoubleColumn.java b/cli/src/main/java/bisq/cli/table/column/DoubleColumn.java new file mode 100644 index 0000000000..01313ebf17 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/DoubleColumn.java @@ -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 . + */ + +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 { + + protected final List rows = new ArrayList<>(); + + protected final Predicate 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 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; + } +} diff --git a/cli/src/main/java/bisq/cli/table/column/FiatColumn.java b/cli/src/main/java/bisq/cli/table/column/FiatColumn.java new file mode 100644 index 0000000000..9e2318c512 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/FiatColumn.java @@ -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 . + */ + +package bisq.cli.table.column; + +import java.util.stream.IntStream; + +import static bisq.cli.CurrencyFormat.formatFiatVolume; +import static bisq.cli.CurrencyFormat.formatPrice; +import static bisq.cli.table.column.Column.JUSTIFICATION.RIGHT; +import static bisq.cli.table.column.FiatColumn.DISPLAY_MODE.FIAT_PRICE; + +/** + * For displaying fiat volume or price with appropriate precision. + */ +public class FiatColumn extends LongColumn { + + public enum DISPLAY_MODE { + FIAT_PRICE, + FIAT_VOLUME + } + + private final DISPLAY_MODE displayMode; + + // The default FiatColumn JUSTIFICATION is RIGHT. + // The default FiatColumn DISPLAY_MODE is PRICE. + public FiatColumn(String name) { + this(name, RIGHT, FIAT_PRICE); + } + + public FiatColumn(String name, DISPLAY_MODE displayMode) { + this(name, RIGHT, displayMode); + } + + public FiatColumn(String name, + JUSTIFICATION justification, + DISPLAY_MODE displayMode) { + super(name, justification); + this.displayMode = displayMode; + } + + @Override + public void addRow(Long value) { + rows.add(value); + + String s = displayMode.equals(FIAT_PRICE) ? formatPrice(value) : formatFiatVolume(value); + + stringColumn.addRow(s); + + if (isNewMaxWidth.test(s)) + maxWidth = s.length(); + } + + @Override + public String getRowAsFormattedString(int rowIndex) { + return getRow(rowIndex).toString(); + } + + @Override + public StringColumn asStringColumn() { + // We cached the formatted fiat price 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; + } +} diff --git a/cli/src/main/java/bisq/cli/table/column/IntegerColumn.java b/cli/src/main/java/bisq/cli/table/column/IntegerColumn.java new file mode 100644 index 0000000000..ed64c19c69 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/IntegerColumn.java @@ -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 . + */ + +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 Integer values. + */ +public class IntegerColumn extends NumberColumn { + + protected final List rows = new ArrayList<>(); + + protected final Predicate isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth; + + // The default IntegerColumn JUSTIFICATION is RIGHT. + public IntegerColumn(String name) { + this(name, RIGHT); + } + + public IntegerColumn(String name, JUSTIFICATION justification) { + super(name, justification); + this.maxWidth = name.length(); + } + + @Override + public void addRow(Integer value) { + rows.add(value); + + String s = String.valueOf(value); + if (isNewMaxWidth.test(s)) + maxWidth = s.length(); + } + + @Override + public List getRows() { + return rows; + } + + @Override + public int rowCount() { + return rows.size(); + } + + @Override + public boolean isEmpty() { + return rows.isEmpty(); + } + + @Override + public Integer getRow(int rowIndex) { + return rows.get(rowIndex); + } + + @Override + public void updateRow(int rowIndex, Integer 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; + } +} diff --git a/cli/src/main/java/bisq/cli/table/column/Iso8601DateTimeColumn.java b/cli/src/main/java/bisq/cli/table/column/Iso8601DateTimeColumn.java new file mode 100644 index 0000000000..a7f60250cc --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/Iso8601DateTimeColumn.java @@ -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 . + */ + +package bisq.cli.table.column; + +import java.text.SimpleDateFormat; + +import java.util.Date; +import java.util.stream.IntStream; + +import static bisq.cli.table.column.Column.JUSTIFICATION.LEFT; +import static com.google.common.base.Strings.padEnd; +import static com.google.common.base.Strings.padStart; +import static java.lang.System.currentTimeMillis; +import static java.util.TimeZone.getTimeZone; + +/** + * For displaying (long) timestamp values as ISO-8601 dates in UTC time zone. + */ +public class Iso8601DateTimeColumn extends LongColumn { + + protected final SimpleDateFormat iso8601DateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + // The default Iso8601DateTimeColumn JUSTIFICATION is LEFT. + public Iso8601DateTimeColumn(String name) { + this(name, LEFT); + } + + public Iso8601DateTimeColumn(String name, JUSTIFICATION justification) { + super(name, justification); + iso8601DateFormat.setTimeZone(getTimeZone("UTC")); + this.maxWidth = Math.max(name.length(), String.valueOf(currentTimeMillis()).length()); + } + + @Override + public String getRowAsFormattedString(int rowIndex) { + long time = getRow(rowIndex); + return justification.equals(LEFT) + ? padEnd(iso8601DateFormat.format(new Date(time)), maxWidth, ' ') + : padStart(iso8601DateFormat.format(new Date(time)), maxWidth, ' '); + } + + @Override + public StringColumn asStringColumn() { + IntStream.range(0, rows.size()).forEachOrdered(rowIndex -> + stringColumn.addRow(getRowAsFormattedString(rowIndex))); + + return stringColumn; + } +} diff --git a/cli/src/main/java/bisq/cli/table/column/LongColumn.java b/cli/src/main/java/bisq/cli/table/column/LongColumn.java new file mode 100644 index 0000000000..445c812307 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/LongColumn.java @@ -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 . + */ + +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 Long values. + */ +public class LongColumn extends NumberColumn { + + protected final List rows = new ArrayList<>(); + + protected final Predicate isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth; + + // The default LongColumn JUSTIFICATION is RIGHT. + public LongColumn(String name) { + this(name, RIGHT); + } + + public LongColumn(String name, JUSTIFICATION justification) { + super(name, justification); + this.maxWidth = name.length(); + } + + @Override + public void addRow(Long value) { + rows.add(value); + + String s = String.valueOf(value); + if (isNewMaxWidth.test(s)) + maxWidth = s.length(); + } + + @Override + public List getRows() { + return rows; + } + + @Override + public int rowCount() { + return rows.size(); + } + + @Override + public boolean isEmpty() { + return rows.isEmpty(); + } + + @Override + public Long getRow(int rowIndex) { + return rows.get(rowIndex); + } + + @Override + public void updateRow(int rowIndex, Long 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; + } +} diff --git a/cli/src/main/java/bisq/cli/table/column/MixedTradeFeeColumn.java b/cli/src/main/java/bisq/cli/table/column/MixedTradeFeeColumn.java new file mode 100644 index 0000000000..36efdb0b82 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/MixedTradeFeeColumn.java @@ -0,0 +1,59 @@ +/* + * 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 . + */ + +package bisq.cli.table.column; + +import static bisq.cli.CurrencyFormat.formatBsq; +import static bisq.cli.CurrencyFormat.formatSatoshis; +import static bisq.cli.table.column.Column.JUSTIFICATION.RIGHT; + +/** + * For displaying a mix of BSQ and BTC trade fees with appropriate precision. + */ +public class MixedTradeFeeColumn extends LongColumn { + + public MixedTradeFeeColumn(String name) { + super(name, RIGHT); + } + + @Override + public void addRow(Long value) { + throw new UnsupportedOperationException("use public void addRow(Long value, boolean isBsq) instead"); + } + + public void addRow(Long value, boolean isBsq) { + rows.add(value); + + String s = isBsq + ? formatBsq(value) + " BSQ" + : formatSatoshis(value) + " BTC"; + stringColumn.addRow(s); + + if (isNewMaxWidth.test(s)) + maxWidth = s.length(); + } + + @Override + public String getRowAsFormattedString(int rowIndex) { + return getRow(rowIndex).toString(); + } + + @Override + public StringColumn asStringColumn() { + return stringColumn.justify(); + } +} diff --git a/cli/src/main/java/bisq/cli/table/column/NumberColumn.java b/cli/src/main/java/bisq/cli/table/column/NumberColumn.java new file mode 100644 index 0000000000..fcd670fbf2 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/NumberColumn.java @@ -0,0 +1,32 @@ +/* + * 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 . + */ + +package bisq.cli.table.column; + +/** + * Abstract superclass for numeric Columns. + * + * @param the subclass column's type (LongColumn, IntegerColumn, ...) + * @param the subclass column's numeric Java type (Long, Integer, ...) + */ +abstract class NumberColumn, + T extends Number> extends AbstractColumn implements Column { + + public NumberColumn(String name, JUSTIFICATION justification) { + super(name, justification); + } +} diff --git a/cli/src/main/java/bisq/cli/table/column/SatoshiColumn.java b/cli/src/main/java/bisq/cli/table/column/SatoshiColumn.java new file mode 100644 index 0000000000..c1501af0c3 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/SatoshiColumn.java @@ -0,0 +1,72 @@ +/* + * 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 . + */ + +package bisq.cli.table.column; + +import static bisq.cli.CurrencyFormat.formatBsq; +import static bisq.cli.CurrencyFormat.formatSatoshis; +import static bisq.cli.table.column.Column.JUSTIFICATION.RIGHT; + +/** + * For displaying BTC or BSQ satoshi values with appropriate precision. + */ +public class SatoshiColumn extends LongColumn { + + protected final boolean isBsqSatoshis; + + // The default SatoshiColumn JUSTIFICATION is RIGHT. + public SatoshiColumn(String name) { + this(name, RIGHT, false); + } + + public SatoshiColumn(String name, boolean isBsqSatoshis) { + this(name, RIGHT, isBsqSatoshis); + } + + public SatoshiColumn(String name, JUSTIFICATION justification) { + this(name, justification, false); + } + + public SatoshiColumn(String name, JUSTIFICATION justification, boolean isBsqSatoshis) { + super(name, justification); + this.isBsqSatoshis = isBsqSatoshis; + } + + @Override + public void addRow(Long value) { + rows.add(value); + + // We do not know how much padding each StringColumn value needs until it has all the values. + String s = isBsqSatoshis ? formatBsq(value) : formatSatoshis(value); + stringColumn.addRow(s); + + if (isNewMaxWidth.test(s)) + maxWidth = s.length(); + } + + @Override + public String getRowAsFormattedString(int rowIndex) { + return isBsqSatoshis + ? formatBsq(getRow(rowIndex)) + : formatSatoshis(getRow(rowIndex)); + } + + @Override + public StringColumn asStringColumn() { + return stringColumn.justify(); + } +} diff --git a/cli/src/main/java/bisq/cli/table/column/StringColumn.java b/cli/src/main/java/bisq/cli/table/column/StringColumn.java new file mode 100644 index 0000000000..97a3fc59f2 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/StringColumn.java @@ -0,0 +1,102 @@ +/* + * 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 . + */ + +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; +import static bisq.cli.table.column.Column.JUSTIFICATION.RIGHT; + +/** + * For displaying justified string values. + */ +public class StringColumn extends AbstractColumn { + + private final List rows = new ArrayList<>(); + + private final Predicate isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth; + + // The default StringColumn JUSTIFICATION is LEFT. + public StringColumn(String name) { + this(name, LEFT); + } + + // Use this constructor to override default LEFT justification. + public StringColumn(String name, JUSTIFICATION justification) { + super(name, justification); + this.maxWidth = name.length(); + } + + @Override + public void addRow(String value) { + rows.add(value); + if (isNewMaxWidth.test(value)) + maxWidth = value.length(); + } + + @Override + public List getRows() { + return rows; + } + + @Override + public int rowCount() { + return rows.size(); + } + + @Override + public boolean isEmpty() { + return rows.isEmpty(); + } + + @Override + public String getRow(int rowIndex) { + return rows.get(rowIndex); + } + + @Override + public void updateRow(int rowIndex, String newValue) { + rows.set(rowIndex, newValue); + } + + @Override + public String getRowAsFormattedString(int rowIndex) { + return getRow(rowIndex); + } + + @Override + public StringColumn asStringColumn() { + return this; + } + + @Override + public StringColumn justify() { + if (justification.equals(RIGHT)) { + IntStream.range(0, getRows().size()).forEach(rowIndex -> { + String unjustified = getRow(rowIndex); + String justified = toJustifiedString(unjustified); + updateRow(rowIndex, justified); + }); + } + return this; + } +} diff --git a/cli/src/main/java/bisq/cli/table/column/ZippedStringColumns.java b/cli/src/main/java/bisq/cli/table/column/ZippedStringColumns.java new file mode 100644 index 0000000000..6f9278f6b6 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/ZippedStringColumns.java @@ -0,0 +1,130 @@ +/* + * 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 . + */ + +package bisq.cli.table.column; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + +import javax.annotation.Nullable; + +import static bisq.cli.table.column.ZippedStringColumns.DUPLICATION_MODE.EXCLUDE_DUPLICATES; +import static bisq.cli.table.column.ZippedStringColumns.DUPLICATION_MODE.INCLUDE_DUPLICATES; + + + +import bisq.cli.table.column.Column.JUSTIFICATION; + +/** + * For zipping multiple StringColumns into a single StringColumn. + * Useful for displaying amount and volume range values. + */ +public class ZippedStringColumns { + + public enum DUPLICATION_MODE { + EXCLUDE_DUPLICATES, + INCLUDE_DUPLICATES + } + + private final String name; + private final JUSTIFICATION justification; + private final String delimiter; + private final StringColumn[] columns; + + public ZippedStringColumns(String name, + JUSTIFICATION justification, + String delimiter, + StringColumn... columns) { + this.name = name; + this.justification = justification; + this.delimiter = delimiter; + this.columns = columns; + validateColumnData(); + } + + public StringColumn asStringColumn(DUPLICATION_MODE duplicationMode) { + StringColumn stringColumn = new StringColumn(name, justification); + + buildRows(stringColumn, duplicationMode); + + // Re-set the column name field to its justified value, in case any of the column + // values are longer than the name passed to this constructor. + stringColumn.setName(stringColumn.toJustifiedString(name)); + + return stringColumn; + } + + private void buildRows(StringColumn stringColumn, DUPLICATION_MODE duplicationMode) { + // Populate the StringColumn with unjustified zipped values; we cannot justify + // the zipped values until stringColumn knows its final maxWidth. + IntStream.range(0, columns[0].getRows().size()).forEach(rowIndex -> { + String row = buildRow(rowIndex, duplicationMode); + stringColumn.addRow(row); + }); + + formatRows(stringColumn); + } + + private String buildRow(int rowIndex, DUPLICATION_MODE duplicationMode) { + StringBuilder rowBuilder = new StringBuilder(); + @Nullable + List processedValues = duplicationMode.equals(EXCLUDE_DUPLICATES) + ? new ArrayList<>() + : null; + IntStream.range(0, columns.length).forEachOrdered(colIndex -> { + // For each column @ rowIndex ... + var value = columns[colIndex].getRows().get(rowIndex); + if (duplicationMode.equals(INCLUDE_DUPLICATES)) { + if (rowBuilder.length() > 0) + rowBuilder.append(delimiter); + + rowBuilder.append(value); + } else if (!processedValues.contains(value)) { + if (rowBuilder.length() > 0) + rowBuilder.append(delimiter); + + rowBuilder.append(value); + processedValues.add(value); + } + }); + return rowBuilder.toString(); + } + + private void formatRows(StringColumn stringColumn) { + // Now we can justify the zipped string values in the new StringColumn. + IntStream.range(0, stringColumn.getRows().size()).forEach(rowIndex -> { + String unjustified = stringColumn.getRow(rowIndex); + String justified = stringColumn.toJustifiedString(unjustified); + stringColumn.updateRow(rowIndex, justified); + }); + } + + private void validateColumnData() { + if (columns.length == 0) + throw new IllegalStateException("cannot zip columns because they do not have any data"); + + StringColumn firstColumn = columns[0]; + if (firstColumn.getRows().isEmpty()) + throw new IllegalStateException("1st column has no data"); + + IntStream.range(1, columns.length).forEach(colIndex -> { + if (columns[colIndex].getRows().size() != firstColumn.getRows().size()) + throw new IllegalStateException("columns do not have same number of rows"); + }); + } +} diff --git a/cli/src/test/java/bisq/cli/AbstractCliTest.java b/cli/src/test/java/bisq/cli/AbstractCliTest.java new file mode 100644 index 0000000000..8fc279295f --- /dev/null +++ b/cli/src/test/java/bisq/cli/AbstractCliTest.java @@ -0,0 +1,268 @@ +package bisq.cli; + +import bisq.proto.grpc.OfferInfo; + +import joptsimple.OptionParser; +import joptsimple.OptionSet; + +import java.math.BigDecimal; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.cli.opts.OptLabel.OPT_HOST; +import static bisq.cli.opts.OptLabel.OPT_PASSWORD; +import static bisq.cli.opts.OptLabel.OPT_PORT; +import static java.lang.System.out; +import static java.math.RoundingMode.HALF_UP; +import static java.util.Arrays.stream; +import static org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch.Operation.DELETE; +import static org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch.Operation.INSERT; + + + +import bisq.cli.opts.ArgumentList; +import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch; + +/** + * Parent class for CLI smoke tests. Useful for examining the format of the console + * output, and checking for diffs while making changes to console output formatters. + * + * Tests that create offers or trades should not be run on mainnet. + */ +@Slf4j +public abstract class AbstractCliTest { + + static final String PASSWORD_OPT = "--password=xyz"; // Both daemons' password. + static final String ALICE_PORT_OPT = "--port=" + 9998; // Alice's daemon port. + static final String BOB_PORT_OPT = "--port=" + 9999; // Bob's daemon port. + static final String[] BASE_ALICE_CLIENT_OPTS = new String[]{PASSWORD_OPT, ALICE_PORT_OPT}; + static final String[] BASE_BOB_CLIENT_OPTS = new String[]{PASSWORD_OPT, BOB_PORT_OPT}; + + protected final BiFunction> randomMarginBasedPrices = (min, max) -> + IntStream.range(min, max).asDoubleStream() + .boxed() + .map(d -> d / 100) + .map(Object::toString) + .collect(Collectors.toList()); + + protected final BiFunction randomFixedAltcoinPrice = (min, max) -> { + String random = Double.valueOf(ThreadLocalRandom.current().nextDouble(min, max)).toString(); + BigDecimal bd = new BigDecimal(random).setScale(8, HALF_UP); + return bd.toPlainString(); + }; + + protected final GrpcClient aliceClient; + protected final GrpcClient bobClient; + + public AbstractCliTest() { + this.aliceClient = getGrpcClient(BASE_ALICE_CLIENT_OPTS); + this.bobClient = getGrpcClient(BASE_BOB_CLIENT_OPTS); + } + + protected GrpcClient getGrpcClient(String[] args) { + var parser = new OptionParser(); + var hostOpt = parser.accepts(OPT_HOST, "rpc server hostname or ip") + .withRequiredArg() + .defaultsTo("localhost"); + var portOpt = parser.accepts(OPT_PORT, "rpc server port") + .withRequiredArg() + .ofType(Integer.class) + .defaultsTo(9998); + var passwordOpt = parser.accepts(OPT_PASSWORD, "rpc server password") + .withRequiredArg(); + + OptionSet options = parser.parse(new ArgumentList(args).getCLIArguments()); + var host = options.valueOf(hostOpt); + var port = options.valueOf(portOpt); + var password = options.valueOf(passwordOpt); + if (password == null) + throw new IllegalArgumentException("missing required 'password' option"); + + return new GrpcClient(host, port, password); + } + + protected void checkDiffsIgnoreWhitespace(String oldOutput, String newOutput) { + Predicate isInsertOrDelete = (operation) -> + operation.equals(INSERT) || operation.equals(DELETE); + Predicate isWhitespace = (text) -> text.trim().isEmpty(); + boolean hasNonWhitespaceDiffs = false; + if (!oldOutput.equals(newOutput)) { + DiffMatchPatch dmp = new DiffMatchPatch(); + LinkedList diff = dmp.diffMain(oldOutput, newOutput, true); + for (DiffMatchPatch.Diff d : diff) { + if (isInsertOrDelete.test(d.operation) && !isWhitespace.test(d.text)) { + hasNonWhitespaceDiffs = true; + log.error(">>> DIFF {}", d); + } + } + } + + if (hasNonWhitespaceDiffs) + log.error("FAIL: There were diffs"); + else + log.info("PASS: No diffs"); + } + + protected void printOldTbl(String tbl) { + log.info("OLD Console OUT:\n{}", tbl); + } + + protected void printNewTbl(String tbl) { + log.info("NEW Console OUT:\n{}", tbl); + } + + protected List getMyAltcoinOffers(String currencyCode) { + String[] args = getMyOffersCommand("buy", currencyCode); + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + + args = getMyOffersCommand("sell", currencyCode); + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + + return aliceClient.getMyOffersSortedByDate(currencyCode); + } + + protected String[] getMyOffersCommand(String direction, String currencyCode) { + return new String[]{ + PASSWORD_OPT, + ALICE_PORT_OPT, + "getmyoffers", + "--direction=" + direction, + "--currency-code=" + currencyCode + }; + } + + protected String[] getAvailableOffersCommand(String direction, String currencyCode) { + return new String[]{ + PASSWORD_OPT, + BOB_PORT_OPT, + "getoffers", + "--direction=" + direction, + "--currency-code=" + currencyCode + }; + } + + + protected void editOfferPriceMargin(OfferInfo offer, String priceMargin, boolean enable) { + String[] args = new String[]{ + PASSWORD_OPT, + ALICE_PORT_OPT, + "editoffer", + "--offer-id=" + offer.getId(), + "--market-price-margin=" + priceMargin, + "--enable=" + enable + }; + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + } + + protected void editOfferTriggerPrice(OfferInfo offer, String triggerPrice, boolean enable) { + String[] args = new String[]{ + PASSWORD_OPT, + ALICE_PORT_OPT, + "editoffer", + "--offer-id=" + offer.getId(), + "--trigger-price=" + triggerPrice, + "--enable=" + enable + }; + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + } + + protected void editOfferPriceMarginAndTriggerPrice(OfferInfo offer, + String priceMargin, + String triggerPrice, + boolean enable) { + String[] args = new String[]{ + PASSWORD_OPT, + ALICE_PORT_OPT, + "editoffer", + "--offer-id=" + offer.getId(), + "--market-price-margin=" + priceMargin, + "--trigger-price=" + triggerPrice, + "--enable=" + enable + }; + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + } + + protected void editOfferFixedPrice(OfferInfo offer, String fixedPrice, boolean enable) { + String[] args = new String[]{ + PASSWORD_OPT, + ALICE_PORT_OPT, + "editoffer", + "--offer-id=" + offer.getId(), + "--fixed-price=" + fixedPrice, + "--enable=" + enable + }; + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + } + + protected void disableOffers(List offers) { + out.println("Disable Offers"); + for (OfferInfo offer : offers) { + editOfferEnable(offer, false); + sleep(5); + } + } + + protected void enableOffers(List offers) { + out.println("Enable Offers"); + for (OfferInfo offer : offers) { + editOfferEnable(offer, true); + sleep(5); + } + } + + protected void editOfferEnable(OfferInfo offer, boolean enable) { + String[] args = new String[]{ + PASSWORD_OPT, + ALICE_PORT_OPT, + "editoffer", + "--offer-id=" + offer.getId(), + "--enable=" + enable + }; + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + } + + protected void sleep(long seconds) { + try { + TimeUnit.SECONDS.sleep(seconds); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/cli/src/test/java/bisq/cli/CreateOfferSmokeTest.java b/cli/src/test/java/bisq/cli/CreateOfferSmokeTest.java new file mode 100644 index 0000000000..3b8a459660 --- /dev/null +++ b/cli/src/test/java/bisq/cli/CreateOfferSmokeTest.java @@ -0,0 +1,66 @@ +package bisq.cli; + +import static java.lang.System.out; +import static java.util.Arrays.stream; + +/** + Smoke tests for createoffer method. Useful for testing CLI command and examining the + format of its console output. + + Prerequisites: + + - Run `./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false --enableBisqDebugging=false` + + Note: Test harness will not automatically generate BTC blocks to confirm transactions. + + Never run on mainnet! + */ +@SuppressWarnings({"CommentedOutCode", "unused"}) +public class CreateOfferSmokeTest extends AbstractCliTest { + + public static void main(String[] args) { + CreateOfferSmokeTest test = new CreateOfferSmokeTest(); + test.createBsqSwapOffer("buy"); + test.createBsqSwapOffer("sell"); + } + + private void createBsqSwapOffer(String direction) { + String[] args = createBsqSwapOfferCommand(direction, "0.01", "0.005", "0.00005"); + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + + args = getMyOffersCommand(direction, "bsq"); + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + + args = getAvailableOffersCommand(direction, "bsq"); + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + } + + private String[] createBsqSwapOfferCommand(String direction, + String amount, + String minAmount, + String fixedPrice) { + return new String[]{ + PASSWORD_OPT, + ALICE_PORT_OPT, + "createoffer", + "--swap=true", + "--direction=" + direction, + "--currency-code=bsq", + "--amount=" + amount, + "--min-amount=" + minAmount, + "--fixed-price=" + fixedPrice + }; + } +} diff --git a/cli/src/test/java/bisq/cli/EditXmrOffersSmokeTest.java b/cli/src/test/java/bisq/cli/EditXmrOffersSmokeTest.java new file mode 100644 index 0000000000..afcc1446cf --- /dev/null +++ b/cli/src/test/java/bisq/cli/EditXmrOffersSmokeTest.java @@ -0,0 +1,100 @@ +package bisq.cli; + +import bisq.proto.grpc.OfferInfo; + +import java.util.List; +import java.util.Random; + +import static java.lang.System.out; +import static protobuf.OfferDirection.BUY; + +/** + Smoke tests for the editoffer method. + + Prerequisites: + + - Run `./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdesktop --shutdownAfterTests=false --enableBisqDebugging=false` + + - Create some XMR offers with Alice's UI or CLI. + + - Watch Alice's offers being edited in Bob's UI. + + Never run on mainnet. + */ +public class EditXmrOffersSmokeTest extends AbstractCliTest { + + public static void main(String[] args) { + var test = new EditXmrOffersSmokeTest(); + + test.doOfferPriceEdits(); + + List offers = test.getMyAltcoinOffers("xmr"); + test.disableOffers(offers); + + test.sleep(6); + + offers = test.getMyAltcoinOffers("xmr"); + test.enableOffers(offers); + + // A final look after last edit. + test.getMyAltcoinOffers("xmr"); + } + + private void doOfferPriceEdits() { + editPriceMargin(); + editTriggerPrice(); + editPriceMarginAndTriggerPrice(); + editFixedPrice(); + } + + private void editPriceMargin() { + var offers = getMyAltcoinOffers("xmr"); + out.println("Edit XMR offers' price margin"); + var margins = randomMarginBasedPrices.apply(-301, 300); + for (int i = 0; i < offers.size(); i++) { + String randomMargin = margins.get(new Random().nextInt(margins.size())); + editOfferPriceMargin(offers.get(i), randomMargin, new Random().nextBoolean()); + sleep(5); + } + } + + private void editTriggerPrice() { + var offers = getMyAltcoinOffers("xmr"); + out.println("Edit XMR offers' trigger price"); + for (int i = 0; i < offers.size(); i++) { + var offer = offers.get(i); + if (offer.getUseMarketBasedPrice()) { + // Trigger price is hardcode to be a bit above or below xmr mkt price at runtime. + // It could be looked up and calculated instead. + var newTriggerPrice = offer.getDirection().equals(BUY.name()) ? "0.0039" : "0.005"; + editOfferTriggerPrice(offer, newTriggerPrice, true); + sleep(5); + } + } + } + + private void editPriceMarginAndTriggerPrice() { + var offers = getMyAltcoinOffers("xmr"); + out.println("Edit XMR offers' price margin and trigger price"); + for (int i = 0; i < offers.size(); i++) { + var offer = offers.get(i); + if (offer.getUseMarketBasedPrice()) { + // Trigger price is hardcode to be a bit above or below xmr mkt price at runtime. + // It could be looked up and calculated instead. + var newTriggerPrice = offer.getDirection().equals(BUY.name()) ? "0.0038" : "0.0051"; + editOfferPriceMarginAndTriggerPrice(offer, "0.05", newTriggerPrice, true); + sleep(5); + } + } + } + + private void editFixedPrice() { + var offers = getMyAltcoinOffers("xmr"); + out.println("Edit XMR offers' fixed price"); + for (int i = 0; i < offers.size(); i++) { + String randomFixedPrice = randomFixedAltcoinPrice.apply(0.004, 0.0075); + editOfferFixedPrice(offers.get(i), randomFixedPrice, new Random().nextBoolean()); + sleep(5); + } + } +} diff --git a/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java b/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java index f613aea358..538587a3de 100644 --- a/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java +++ b/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java @@ -11,15 +11,49 @@ import static java.lang.System.out; This can be run on mainnet. */ -public class GetOffersSmokeTest { +@SuppressWarnings({"CommentedOutCode", "unused"}) +public class GetOffersSmokeTest extends AbstractCliTest { + + // TODO use the static password and port opt definitions in superclass public static void main(String[] args) { + getMyBsqOffers(); + // getAvailableBsqOffers(); + // getMyUsdOffers(); + // getAvailableUsdOffers(); + } + private static void getMyBsqOffers() { + out.println(">>> getmyoffers buy bsq"); + CliMain.main(new String[]{"--password=xyz", "--port=9998", "getmyoffers", "--direction=buy", "--currency-code=bsq"}); + out.println(">>> getmyoffers sell bsq"); + CliMain.main(new String[]{"--password=xyz", "--port=9998", "getmyoffers", "--direction=sell", "--currency-code=bsq"}); + out.println(">>> getmyoffer --offer-id=KRONTTMO-11cef1a9-c636-4dc7-b3f2-1616e4960c28-175"); + CliMain.main(new String[]{"--password=xyz", "--port=9998", "getmyoffer", "--offer-id=KRONTTMO-11cef1a9-c636-4dc7-b3f2-1616e4960c28-175"}); + } + + private static void getAvailableBsqOffers() { + out.println(">>> getoffers buy bsq"); + CliMain.main(new String[]{"--password=xyz", "--port=9998", "getoffers", "--direction=buy", "--currency-code=bsq"}); + out.println(">>> getoffers sell bsq"); + CliMain.main(new String[]{"--password=xyz", "--port=9998", "getoffers", "--direction=sell", "--currency-code=bsq"}); + } + + private static void getMyUsdOffers() { + out.println(">>> getmyoffers buy usd"); + CliMain.main(new String[]{"--password=xyz", "--port=9998", "getmyoffers", "--direction=buy", "--currency-code=usd"}); + out.println(">>> getmyoffers sell usd"); + CliMain.main(new String[]{"--password=xyz", "--port=9998", "getmyoffers", "--direction=sell", "--currency-code=usd"}); + } + + private static void getAvailableUsdOffers() { out.println(">>> getoffers buy usd"); - CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=usd"}); + CliMain.main(new String[]{"--password=xyz", "--port=9998", "getoffers", "--direction=buy", "--currency-code=usd"}); out.println(">>> getoffers sell usd"); - CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=usd"}); + CliMain.main(new String[]{"--password=xyz", "--port=9998", "getoffers", "--direction=sell", "--currency-code=usd"}); + } + private static void TODO() { out.println(">>> getoffers buy eur"); CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=eur"}); out.println(">>> getoffers sell eur"); @@ -35,5 +69,4 @@ public class GetOffersSmokeTest { out.println(">>> getoffers sell brl"); CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=brl"}); } - } diff --git a/cli/src/test/java/bisq/cli/GetTradesSmokeTest.java b/cli/src/test/java/bisq/cli/GetTradesSmokeTest.java new file mode 100644 index 0000000000..a95b734a87 --- /dev/null +++ b/cli/src/test/java/bisq/cli/GetTradesSmokeTest.java @@ -0,0 +1,52 @@ +package bisq.cli; + +import bisq.proto.grpc.TradeInfo; + +import java.util.List; + +import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL; +import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED; +import static java.lang.System.out; + + + +import bisq.cli.table.builder.TableBuilder; + +@SuppressWarnings("unused") +public class GetTradesSmokeTest extends AbstractCliTest { + + public static void main(String[] args) { + GetTradesSmokeTest test = new GetTradesSmokeTest(); + test.printAlicesTrades(); + test.printBobsTrades(); + } + + private final List openTrades; + private final List closedTrades; + + public GetTradesSmokeTest() { + super(); + this.openTrades = aliceClient.getOpenTrades(); + this.closedTrades = aliceClient.getTradeHistory(CLOSED); + } + + private void printAlicesTrades() { + out.println("ALICE'S OPEN TRADES"); + openTrades.stream().forEachOrdered(t -> printTrade(aliceClient, t.getTradeId())); + out.println("ALICE'S CLOSED TRADES"); + closedTrades.stream().forEachOrdered(t -> printTrade(aliceClient, t.getTradeId())); + } + + private void printBobsTrades() { + out.println("BOB'S OPEN TRADES"); + openTrades.stream().forEachOrdered(t -> printTrade(bobClient, t.getTradeId())); + out.println("BOB'S CLOSED TRADES"); + closedTrades.stream().forEachOrdered(t -> printTrade(bobClient, t.getTradeId())); + } + + private void printTrade(GrpcClient client, String tradeId) { + var trade = client.getTrade(tradeId); + var tbl = new TableBuilder(TRADE_DETAIL_TBL, trade).build().toString(); + out.println(tbl); + } +} diff --git a/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java b/cli/src/test/java/bisq/cli/opts/OptionParsersTest.java similarity index 59% rename from cli/src/test/java/bisq/cli/opt/OptionParsersTest.java rename to cli/src/test/java/bisq/cli/opts/OptionParsersTest.java index 983e3b39fb..04b82bafd4 100644 --- a/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java +++ b/cli/src/test/java/bisq/cli/opts/OptionParsersTest.java @@ -1,4 +1,4 @@ -package bisq.cli.opt; +package bisq.cli.opts; import org.junit.jupiter.api.Test; @@ -9,13 +9,7 @@ import static bisq.cli.Method.createpaymentacct; import static bisq.cli.opts.OptLabel.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; - - - -import bisq.cli.opts.CancelOfferOptionParser; -import bisq.cli.opts.CreateCryptoCurrencyPaymentAcctOptionParser; -import bisq.cli.opts.CreateOfferOptionParser; -import bisq.cli.opts.CreatePaymentAcctOptionParser; +import static org.junit.jupiter.api.Assertions.assertTrue; public class OptionParsersTest { @@ -69,13 +63,16 @@ public class OptionParsersTest { new CancelOfferOptionParser(args).parse(); } - // createoffer opt parser tests + // createoffer (v1) opt parser tests @Test - public void testCreateOfferOptParserWithMissingPaymentAccountIdOptShouldThrowException() { + public void testCreateOfferWithMissingPaymentAccountIdOptShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, - createoffer.name() + createoffer.name(), + "--" + OPT_DIRECTION + "=" + "SELL", + "--" + OPT_CURRENCY_CODE + "=" + "JPY", + "--" + OPT_AMOUNT + "=" + "0.1" }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreateOfferOptionParser(args).parse()); @@ -83,23 +80,23 @@ public class OptionParsersTest { } @Test - public void testCreateOfferOptParserWithEmptyPaymentAccountIdOptShouldThrowException() { + public void testCreateOfferWithEmptyPaymentAccountIdOptShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createoffer.name(), - "--" + OPT_PAYMENT_ACCOUNT + "--" + OPT_PAYMENT_ACCOUNT_ID }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreateOfferOptionParser(args).parse()); - assertEquals("payment-account requires an argument", exception.getMessage()); + assertEquals("payment-account-id requires an argument", exception.getMessage()); } @Test - public void testCreateOfferOptParserWithMissingDirectionOptShouldThrowException() { + public void testCreateOfferWithMissingDirectionOptShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createoffer.name(), - "--" + OPT_PAYMENT_ACCOUNT + "=" + "abc-payment-acct-id-123" + "--" + OPT_PAYMENT_ACCOUNT_ID + "=" + "abc-payment-acct-id-123" }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreateOfferOptionParser(args).parse()); @@ -108,11 +105,11 @@ public class OptionParsersTest { @Test - public void testCreateOfferOptParserWithMissingDirectionOptValueShouldThrowException() { + public void testCreateOfferWithMissingDirectionOptValueShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createoffer.name(), - "--" + OPT_PAYMENT_ACCOUNT + "=" + "abc-payment-acct-id-123", + "--" + OPT_PAYMENT_ACCOUNT_ID + "=" + "abc-payment-acct-id-123", "--" + OPT_DIRECTION + "=" + "" }; Throwable exception = assertThrows(RuntimeException.class, () -> @@ -125,11 +122,11 @@ public class OptionParsersTest { String[] args = new String[]{ PASSWORD_OPT, createoffer.name(), - "--" + OPT_PAYMENT_ACCOUNT + "=" + "abc-payment-acct-id-123", + "--" + OPT_PAYMENT_ACCOUNT_ID + "=" + "abc-payment-acct-id-123", "--" + OPT_DIRECTION + "=" + "BUY", "--" + OPT_CURRENCY_CODE + "=" + "EUR", "--" + OPT_AMOUNT + "=" + "0.125", - "--" + OPT_MKT_PRICE_MARGIN + "=" + "0.0", + "--" + OPT_MKT_PRICE_MARGIN + "=" + "3.15", "--" + OPT_SECURITY_DEPOSIT + "=" + "25.0" }; CreateOfferOptionParser parser = new CreateOfferOptionParser(args).parse(); @@ -137,14 +134,14 @@ public class OptionParsersTest { assertEquals("BUY", parser.getDirection()); assertEquals("EUR", parser.getCurrencyCode()); assertEquals("0.125", parser.getAmount()); - assertEquals("0.0", parser.getMktPriceMargin()); - assertEquals("25.0", parser.getSecurityDeposit()); + assertEquals(3.15d, parser.getMktPriceMarginPct()); + assertEquals(25.0, parser.getSecurityDepositPct()); } // createpaymentacct opt parser tests @Test - public void testCreatePaymentAcctOptParserWithMissingPaymentFormOptShouldThrowException() { + public void testCreatePaymentAcctWithMissingPaymentFormOptShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createpaymentacct.name() @@ -156,7 +153,7 @@ public class OptionParsersTest { } @Test - public void testCreatePaymentAcctOptParserWithMissingPaymentFormOptValueShouldThrowException() { + public void testCreatePaymentAcctWithMissingPaymentFormOptValueShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createpaymentacct.name(), @@ -168,7 +165,7 @@ public class OptionParsersTest { } @Test - public void testCreatePaymentAcctOptParserWithInvalidPaymentFormOptValueShouldThrowException() { + public void testCreatePaymentAcctWithInvalidPaymentFormOptValueShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createpaymentacct.name(), @@ -178,10 +175,65 @@ public class OptionParsersTest { new CreatePaymentAcctOptionParser(args).parse()); if (System.getProperty("os.name").toLowerCase().indexOf("win") >= 0) assertEquals("json payment account form '\\tmp\\milkyway\\solarsystem\\mars' could not be found", - exception.getMessage()); + exception.getMessage()); else assertEquals("json payment account form '/tmp/milkyway/solarsystem/mars' could not be found", exception.getMessage()); } + // createcryptopaymentacct parser tests + + @Test + public void testCreateCryptoCurrencyPaymentAcctWithMissingAcctNameOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name() + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); + assertEquals("no payment account name specified", exception.getMessage()); + } + + @Test + public void testCreateCryptoCurrencyPaymentAcctWithEmptyAcctNameOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name(), + "--" + OPT_ACCOUNT_NAME + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); + assertEquals("account-name requires an argument", exception.getMessage()); + } + + @Test + public void testCreateCryptoCurrencyPaymentAcctWithInvalidCurrencyCodeOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name(), + "--" + OPT_ACCOUNT_NAME + "=" + "bsq payment account", + "--" + OPT_CURRENCY_CODE + "=" + "bsq" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); + assertEquals("api does not support bsq payment accounts", exception.getMessage()); + } + + @Test + public void testCreateBchPaymentAcct() { + var acctName = "bch payment account"; + var currencyCode = "bch"; + var address = "B1nXyZ46XXX"; // address is validated on server + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name(), + "--" + OPT_ACCOUNT_NAME + "=" + acctName, + "--" + OPT_CURRENCY_CODE + "=" + currencyCode, + "--" + OPT_ADDRESS + "=" + address + }; + var parser = new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse(); + assertEquals(acctName, parser.getAccountName()); + assertEquals(currencyCode, parser.getCurrencyCode()); + assertEquals(address, parser.getAddress()); + } } diff --git a/cli/src/test/java/bisq/cli/table/AddressCliOutputDiffTest.java b/cli/src/test/java/bisq/cli/table/AddressCliOutputDiffTest.java new file mode 100644 index 0000000000..be5fd9de87 --- /dev/null +++ b/cli/src/test/java/bisq/cli/table/AddressCliOutputDiffTest.java @@ -0,0 +1,64 @@ +package bisq.cli.table; + +import bisq.proto.grpc.AddressBalanceInfo; + +import java.util.List; + +import static bisq.cli.table.builder.TableType.ADDRESS_BALANCE_TBL; +import static java.lang.System.err; +import static java.util.Collections.singletonList; + + + +import bisq.cli.AbstractCliTest; +import bisq.cli.table.builder.TableBuilder; + +@SuppressWarnings("unused") +public class AddressCliOutputDiffTest extends AbstractCliTest { + + public static void main(String[] args) { + AddressCliOutputDiffTest test = new AddressCliOutputDiffTest(); + test.getFundingAddresses(); + test.getAddressBalance(); + } + + public AddressCliOutputDiffTest() { + super(); + } + + private void getFundingAddresses() { + var fundingAddresses = aliceClient.getFundingAddresses(); + if (fundingAddresses.size() > 0) { + // TableFormat class had been deprecated, then deleted on 17-Feb-2022, but + // these diff tests can be useful for testing changes to the current tbl formatting api. + // var oldTbl = TableFormat.formatAddressBalanceTbl(fundingAddresses); + var newTbl = new TableBuilder(ADDRESS_BALANCE_TBL, fundingAddresses).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + // checkDiffsIgnoreWhitespace(oldTbl, newTbl); + } else { + err.println("no funding addresses found"); + } + } + + private void getAddressBalance() { + List addresses = aliceClient.getFundingAddresses(); + int numAddresses = addresses.size(); + // Check output for last 2 addresses. + for (int i = numAddresses - 2; i < addresses.size(); i++) { + var addressBalanceInfo = addresses.get(i); + getAddressBalance(addressBalanceInfo.getAddress()); + } + } + + private void getAddressBalance(String address) { + var addressBalance = singletonList(aliceClient.getAddressBalance(address)); + // TableFormat class had been deprecated, then deleted on 17-Feb-2022, but these + // diff tests can be useful for testing changes to the current tbl formatting api. + // var oldTbl = TableFormat.formatAddressBalanceTbl(addressBalance); + var newTbl = new TableBuilder(ADDRESS_BALANCE_TBL, addressBalance).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + // checkDiffsIgnoreWhitespace(oldTbl, newTbl); + } +} diff --git a/cli/src/test/java/bisq/cli/table/GetBalanceCliOutputDiffTest.java b/cli/src/test/java/bisq/cli/table/GetBalanceCliOutputDiffTest.java new file mode 100644 index 0000000000..bc4dbf972e --- /dev/null +++ b/cli/src/test/java/bisq/cli/table/GetBalanceCliOutputDiffTest.java @@ -0,0 +1,32 @@ +package bisq.cli.table; + +import static bisq.cli.table.builder.TableType.BTC_BALANCE_TBL; + + + +import bisq.cli.AbstractCliTest; +import bisq.cli.table.builder.TableBuilder; + +@SuppressWarnings("unused") +public class GetBalanceCliOutputDiffTest extends AbstractCliTest { + + public static void main(String[] args) { + GetBalanceCliOutputDiffTest test = new GetBalanceCliOutputDiffTest(); + test.getBtcBalance(); + } + + public GetBalanceCliOutputDiffTest() { + super(); + } + + private void getBtcBalance() { + var balance = aliceClient.getBtcBalances(); + // TableFormat class had been deprecated, then deleted on 17-Feb-2022, but these + // diff tests can be useful for testing changes to the current tbl formatting api. + // var oldTbl = TableFormat.formatBtcBalanceInfoTbl(balance); + var newTbl = new TableBuilder(BTC_BALANCE_TBL, balance).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + // checkDiffsIgnoreWhitespace(oldTbl, newTbl); + } +} diff --git a/cli/src/test/java/bisq/cli/table/GetOffersCliOutputDiffTest.java b/cli/src/test/java/bisq/cli/table/GetOffersCliOutputDiffTest.java new file mode 100644 index 0000000000..f5b85bdc62 --- /dev/null +++ b/cli/src/test/java/bisq/cli/table/GetOffersCliOutputDiffTest.java @@ -0,0 +1,127 @@ +package bisq.cli.table; + +import bisq.proto.grpc.OfferInfo; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.cli.table.builder.TableType.OFFER_TBL; +import static protobuf.OfferDirection.BUY; +import static protobuf.OfferDirection.SELL; + + + +import bisq.cli.AbstractCliTest; +import bisq.cli.table.builder.TableBuilder; + +@SuppressWarnings("unused") +@Slf4j +public class GetOffersCliOutputDiffTest extends AbstractCliTest { + + // "My" offers are always Alice's offers. + // "Available" offers are always Alice's offers available to Bob. + + public static void main(String[] args) { + GetOffersCliOutputDiffTest test = new GetOffersCliOutputDiffTest(); + + test.getMyBuyUsdOffers(); + test.getMySellUsdOffers(); + test.getAvailableBuyUsdOffers(); + test.getAvailableSellUsdOffers(); + + /* + // TODO Uncomment when XMR support is added. + test.getMyBuyXmrOffers(); + test.getMySellXmrOffers(); + test.getAvailableBuyXmrOffers(); + test.getAvailableSellXmrOffers(); + */ + + test.getMyBuyBsqOffers(); + test.getMySellBsqOffers(); + test.getAvailableBuyBsqOffers(); + test.getAvailableSellBsqOffers(); + } + + public GetOffersCliOutputDiffTest() { + super(); + } + + private void getMyBuyUsdOffers() { + var myOffers = aliceClient.getMyOffers(BUY.name(), "USD"); + printAndCheckDiffs(myOffers, BUY.name(), "USD"); + } + + private void getMySellUsdOffers() { + var myOffers = aliceClient.getMyOffers(SELL.name(), "USD"); + printAndCheckDiffs(myOffers, SELL.name(), "USD"); + } + + private void getAvailableBuyUsdOffers() { + var offers = bobClient.getOffers(BUY.name(), "USD"); + printAndCheckDiffs(offers, BUY.name(), "USD"); + } + + private void getAvailableSellUsdOffers() { + var offers = bobClient.getOffers(SELL.name(), "USD"); + printAndCheckDiffs(offers, SELL.name(), "USD"); + } + + private void getMyBuyXmrOffers() { + var myOffers = aliceClient.getMyOffers(BUY.name(), "XMR"); + printAndCheckDiffs(myOffers, BUY.name(), "XMR"); + } + + private void getMySellXmrOffers() { + var myOffers = aliceClient.getMyOffers(SELL.name(), "XMR"); + printAndCheckDiffs(myOffers, SELL.name(), "XMR"); + } + + private void getAvailableBuyXmrOffers() { + var offers = bobClient.getOffers(BUY.name(), "XMR"); + printAndCheckDiffs(offers, BUY.name(), "XMR"); + } + + private void getAvailableSellXmrOffers() { + var offers = bobClient.getOffers(SELL.name(), "XMR"); + printAndCheckDiffs(offers, SELL.name(), "XMR"); + } + + private void getMyBuyBsqOffers() { + var myOffers = aliceClient.getMyOffers(BUY.name(), "BSQ"); + printAndCheckDiffs(myOffers, BUY.name(), "BSQ"); + } + + private void getMySellBsqOffers() { + var myOffers = aliceClient.getMyOffers(SELL.name(), "BSQ"); + printAndCheckDiffs(myOffers, SELL.name(), "BSQ"); + } + + private void getAvailableBuyBsqOffers() { + var offers = bobClient.getOffers(BUY.name(), "BSQ"); + printAndCheckDiffs(offers, BUY.name(), "BSQ"); + } + + private void getAvailableSellBsqOffers() { + var offers = bobClient.getOffers(SELL.name(), "BSQ"); + printAndCheckDiffs(offers, SELL.name(), "BSQ"); + } + + private void printAndCheckDiffs(List offers, + String direction, + String currencyCode) { + if (offers.isEmpty()) { + log.warn("No {} {} offers to print.", direction, currencyCode); + } else { + log.info("Checking for diffs in {} {} offers.", direction, currencyCode); + // OfferFormat class had been deprecated, then deleted on 17-Feb-2022, but + // these diff tests can be useful for testing changes to the current tbl formatting api. + // var oldTbl = OfferFormat.formatOfferTable(offers, currencyCode); + var newTbl = new TableBuilder(OFFER_TBL, offers).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + // checkDiffsIgnoreWhitespace(oldTbl, newTbl); + } + } +} diff --git a/cli/src/test/java/bisq/cli/table/GetTradeCliOutputDiffTest.java b/cli/src/test/java/bisq/cli/table/GetTradeCliOutputDiffTest.java new file mode 100644 index 0000000000..25c6ceed5a --- /dev/null +++ b/cli/src/test/java/bisq/cli/table/GetTradeCliOutputDiffTest.java @@ -0,0 +1,53 @@ +package bisq.cli.table; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL; +import static java.lang.System.out; + + + +import bisq.cli.AbstractCliTest; +import bisq.cli.GrpcClient; +import bisq.cli.table.builder.TableBuilder; + +@SuppressWarnings("unused") +@Slf4j +public class GetTradeCliOutputDiffTest extends AbstractCliTest { + + public static void main(String[] args) { + if (args.length == 0) + throw new IllegalStateException("Need a single trade-id program argument."); + + GetTradeCliOutputDiffTest test = new GetTradeCliOutputDiffTest(args[0]); + test.getAlicesTrade(); + out.println(); + test.getBobsTrade(); + } + + private final String tradeId; + + public GetTradeCliOutputDiffTest(String tradeId) { + super(); + this.tradeId = tradeId; + } + + private void getAlicesTrade() { + getTrade(aliceClient); + } + + private void getBobsTrade() { + getTrade(bobClient); + } + + private void getTrade(GrpcClient client) { + var trade = client.getTrade(tradeId); + // TradeFormat class had been deprecated, then deleted on 17-Feb-2022, but these + // diff tests can be useful for testing changes to the current tbl formatting api. + // var oldTbl = TradeFormat.format(trade); + var newTbl = new TableBuilder(TRADE_DETAIL_TBL, trade).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + // checkDiffsIgnoreWhitespace(oldTbl, newTbl); + } +} diff --git a/cli/src/test/java/bisq/cli/table/GetTransactionCliOutputDiffTest.java b/cli/src/test/java/bisq/cli/table/GetTransactionCliOutputDiffTest.java new file mode 100644 index 0000000000..d72749cd24 --- /dev/null +++ b/cli/src/test/java/bisq/cli/table/GetTransactionCliOutputDiffTest.java @@ -0,0 +1,41 @@ +package bisq.cli.table; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.cli.table.builder.TableType.TRANSACTION_TBL; + + + +import bisq.cli.AbstractCliTest; +import bisq.cli.table.builder.TableBuilder; + +@SuppressWarnings("unused") +@Slf4j +public class GetTransactionCliOutputDiffTest extends AbstractCliTest { + + public static void main(String[] args) { + if (args.length == 0) + throw new IllegalStateException("Need a single transaction-id program argument."); + + GetTransactionCliOutputDiffTest test = new GetTransactionCliOutputDiffTest(args[0]); + test.getTransaction(); + } + + private final String transactionId; + + public GetTransactionCliOutputDiffTest(String transactionId) { + super(); + this.transactionId = transactionId; + } + + private void getTransaction() { + var tx = aliceClient.getTransaction(transactionId); + // TransactionFormat class had been deprecated, then deleted on 17-Feb-2022, but + // these diff tests can be useful for testing changes to the current tbl formatting api. + // var oldTbl = TransactionFormat.format(tx); + var newTbl = new TableBuilder(TRANSACTION_TBL, tx).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + // checkDiffsIgnoreWhitespace(oldTbl, newTbl); + } +} diff --git a/cli/src/test/java/bisq/cli/table/PaymentAccountsCliOutputDiffTest.java b/cli/src/test/java/bisq/cli/table/PaymentAccountsCliOutputDiffTest.java new file mode 100644 index 0000000000..30dda73d08 --- /dev/null +++ b/cli/src/test/java/bisq/cli/table/PaymentAccountsCliOutputDiffTest.java @@ -0,0 +1,40 @@ +package bisq.cli.table; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.cli.table.builder.TableType.PAYMENT_ACCOUNT_TBL; + + + +import bisq.cli.AbstractCliTest; +import bisq.cli.table.builder.TableBuilder; + +@SuppressWarnings("unused") +@Slf4j +public class PaymentAccountsCliOutputDiffTest extends AbstractCliTest { + + public static void main(String[] args) { + PaymentAccountsCliOutputDiffTest test = new PaymentAccountsCliOutputDiffTest(); + test.getPaymentAccounts(); + } + + public PaymentAccountsCliOutputDiffTest() { + super(); + } + + private void getPaymentAccounts() { + var paymentAccounts = aliceClient.getPaymentAccounts(); + if (paymentAccounts.size() > 0) { + // The formatPaymentAcctTbl method had been deprecated, then deleted on 17-Feb-2022, + // but these diff tests can be useful for testing changes to the current tbl formatting api. + // var oldTbl = formatPaymentAcctTbl(paymentAccounts); + var newTbl = new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccounts).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + // checkDiffsIgnoreWhitespace(oldTbl, newTbl); + } else { + log.warn("no payment accounts found"); + } + } + +} diff --git a/common/src/main/java/bisq/common/config/Config.java b/common/src/main/java/bisq/common/config/Config.java index 8b5189947b..b424d9ec40 100644 --- a/common/src/main/java/bisq/common/config/Config.java +++ b/common/src/main/java/bisq/common/config/Config.java @@ -122,7 +122,7 @@ public class Config { public static final int UNSPECIFIED_PORT = -1; public static final String DEFAULT_REGTEST_HOST = "none"; public static final int DEFAULT_NUM_CONNECTIONS_FOR_BTC = 9; // down from BitcoinJ default of 12 - static final String DEFAULT_CONFIG_FILE_NAME = "bisq.properties"; + static final String DEFAULT_CONFIG_FILE_NAME = "haveno.properties"; // Static fields that provide access to Config properties in locations where injecting // a Config instance is not feasible. See Javadoc for corresponding static accessors. @@ -599,7 +599,7 @@ public class Config { // Option parsing is strict at the command line, but we relax it now for any // subsequent config file processing. This is for compatibility with pre-1.2.6 - // versions that allowed unrecognized options in the bisq.properties config + // versions that allowed unrecognized options in the haveno.properties config // file and because it follows suit with Bitcoin Core's config file behavior. parser.allowsUnrecognizedOptions(); diff --git a/common/src/main/java/bisq/common/persistence/PersistenceManager.java b/common/src/main/java/bisq/common/persistence/PersistenceManager.java index 0095b6c780..55e9aabe5c 100644 --- a/common/src/main/java/bisq/common/persistence/PersistenceManager.java +++ b/common/src/main/java/bisq/common/persistence/PersistenceManager.java @@ -29,6 +29,7 @@ import bisq.common.file.FileUtil; import bisq.common.handlers.ResultHandler; import bisq.common.proto.persistable.PersistableEnvelope; import bisq.common.proto.persistable.PersistenceProtoResolver; +import bisq.common.util.GcUtil; import bisq.common.util.Utilities; import com.google.inject.Inject; @@ -325,7 +326,11 @@ public class PersistenceManager { new Thread(() -> { T persisted = getPersisted(fileName); if (persisted != null) { - UserThread.execute(() -> resultHandler.accept(persisted)); + UserThread.execute(() -> { + resultHandler.accept(persisted); + + GcUtil.maybeReleaseMemory(); + }); } else { UserThread.execute(orElse); } @@ -434,7 +439,16 @@ public class PersistenceManager { } } + public void forcePersistNow() { + // Tor Bridges settings are edited before app init completes, require persistNow to be forced, see writeToDisk() + persistNow(null, true); + } + public void persistNow(@Nullable Runnable completeHandler) { + persistNow(completeHandler, false); + } + + private void persistNow(@Nullable Runnable completeHandler, boolean force) { long ts = System.currentTimeMillis(); try { // The serialisation is done on the user thread to avoid threading issue with potential mutations of the @@ -444,7 +458,7 @@ public class PersistenceManager { // For the write to disk task we use a thread. We do not have any issues anymore if the persistable objects // gets mutated while the thread is running as we have serialized it already and do not operate on the // reference to the persistable object. - getWriteToDiskExecutor().execute(() -> writeToDisk(serialized, completeHandler)); + getWriteToDiskExecutor().execute(() -> writeToDisk(serialized, completeHandler, force)); long duration = System.currentTimeMillis() - ts; if (duration > 100) { @@ -457,8 +471,8 @@ public class PersistenceManager { } } - public void writeToDisk(protobuf.PersistableEnvelope serialized, @Nullable Runnable completeHandler) { - if (!allServicesInitialized.get()) { + private void writeToDisk(protobuf.PersistableEnvelope serialized, @Nullable Runnable completeHandler, boolean force) { + if (!allServicesInitialized.get() && !force) { log.warn("Application has not completed start up yet so we do not permit writing data to disk."); if (completeHandler != null) { UserThread.execute(completeHandler); diff --git a/common/src/main/java/bisq/common/util/CompletableFutureUtils.java b/common/src/main/java/bisq/common/util/CompletableFutureUtils.java new file mode 100644 index 0000000000..4c34957db2 --- /dev/null +++ b/common/src/main/java/bisq/common/util/CompletableFutureUtils.java @@ -0,0 +1,39 @@ +/* + * 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 . + */ + +package bisq.common.util; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class CompletableFutureUtils { + /** + * @param list List of futures + * @param The generic type of the future + * @return Returns a CompletableFuture with a list of the futures we got as parameter once all futures + * are completed (incl. exceptionally completed). + */ + public static CompletableFuture> allOf(List> list) { + CompletableFuture allFuturesResult = CompletableFuture.allOf(list.toArray(new CompletableFuture[list.size()])); + return allFuturesResult.thenApply(v -> + list.stream(). + map(CompletableFuture::join). + collect(Collectors.toList()) + ); + } +} diff --git a/common/src/main/java/bisq/common/util/GcUtil.java b/common/src/main/java/bisq/common/util/GcUtil.java new file mode 100644 index 0000000000..c7931d3eb9 --- /dev/null +++ b/common/src/main/java/bisq/common/util/GcUtil.java @@ -0,0 +1,85 @@ +/* + * 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 . + */ + +package bisq.common.util; + +import bisq.common.UserThread; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class GcUtil { + @Setter + private static boolean DISABLE_GC_CALLS = false; + private static final int TRIGGER_MEM = 1000; + private static final int TRIGGER_MAX_MEM = 3000; + private static int totalInvocations; + private static long totalGCTime; + + public static void autoReleaseMemory() { + if (DISABLE_GC_CALLS) + return; + + autoReleaseMemory(TRIGGER_MEM); + } + + public static void maybeReleaseMemory() { + if (DISABLE_GC_CALLS) + return; + + maybeReleaseMemory(TRIGGER_MAX_MEM); + } + + /** + * @param trigger Threshold for free memory in MB when we invoke the garbage collector + */ + private static void autoReleaseMemory(long trigger) { + UserThread.runPeriodically(() -> maybeReleaseMemory(trigger), 120); + } + + /** + * @param trigger Threshold for free memory in MB when we invoke the garbage collector + */ + private static void maybeReleaseMemory(long trigger) { + long ts = System.currentTimeMillis(); + long preGcMemory = Runtime.getRuntime().totalMemory(); + if (preGcMemory > trigger * 1024 * 1024) { + System.gc(); + totalInvocations++; + long postGcMemory = Runtime.getRuntime().totalMemory(); + long duration = System.currentTimeMillis() - ts; + totalGCTime += duration; + log.info("GC reduced memory by {}. Total memory before/after: {}/{}. Free memory: {}. Took {} ms. Total GC invocations: {} / Total GC time {} sec", + Utilities.readableFileSize(preGcMemory - postGcMemory), + Utilities.readableFileSize(preGcMemory), + Utilities.readableFileSize(postGcMemory), + Utilities.readableFileSize(Runtime.getRuntime().freeMemory()), + duration, + totalInvocations, + totalGCTime / 1000d); + /* if (DevEnv.isDevMode()) { + try { + // To see from where we got called + throw new RuntimeException("Dummy Exception for print stacktrace at maybeReleaseMemory"); + } catch (Throwable t) { + t.printStackTrace(); + } + }*/ + } + } +} diff --git a/common/src/main/java/bisq/common/util/PermutationUtil.java b/common/src/main/java/bisq/common/util/PermutationUtil.java index 25388619fd..cf96f2faf2 100644 --- a/common/src/main/java/bisq/common/util/PermutationUtil.java +++ b/common/src/main/java/bisq/common/util/PermutationUtil.java @@ -22,7 +22,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiFunction; +import java.util.function.BiPredicate; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @@ -58,9 +58,9 @@ public class PermutationUtil { public static List findMatchingPermutation(R targetValue, List list, - BiFunction, Boolean> predicate, + BiPredicate> predicate, int maxIterations) { - if (predicate.apply(targetValue, list)) { + if (predicate.test(targetValue, list)) { return list; } else { return findMatchingPermutation(targetValue, @@ -74,7 +74,7 @@ public class PermutationUtil { private static List findMatchingPermutation(R targetValue, List list, List> lists, - BiFunction, Boolean> predicate, + BiPredicate> predicate, AtomicInteger maxIterations) { for (int level = 0; level < list.size(); level++) { // Test one level at a time @@ -90,7 +90,7 @@ public class PermutationUtil { @NonNull private static List checkLevel(R targetValue, List previousLevel, - BiFunction, Boolean> predicate, + BiPredicate> predicate, int level, int permutationIndex, AtomicInteger maxIterations) { @@ -106,7 +106,7 @@ public class PermutationUtil { if (level == 0) { maxIterations.decrementAndGet(); // Check all permutations on this level - if (predicate.apply(targetValue, newList)) { + if (predicate.test(targetValue, newList)) { return newList; } } else { diff --git a/common/src/main/java/bisq/common/util/Profiler.java b/common/src/main/java/bisq/common/util/Profiler.java index d3d1ab6541..2851d03c83 100644 --- a/common/src/main/java/bisq/common/util/Profiler.java +++ b/common/src/main/java/bisq/common/util/Profiler.java @@ -17,18 +17,30 @@ package bisq.common.util; +import bisq.common.UserThread; + +import java.util.concurrent.TimeUnit; + import lombok.extern.slf4j.Slf4j; @Slf4j public class Profiler { + public static void printSystemLoadPeriodically(long delay, TimeUnit timeUnit) { + UserThread.runPeriodically(Profiler::printSystemLoad, delay, timeUnit); + } + public static void printSystemLoad() { Runtime runtime = Runtime.getRuntime(); - long free = runtime.freeMemory() / 1024 / 1024; - long total = runtime.totalMemory() / 1024 / 1024; + long free = runtime.freeMemory(); + long total = runtime.totalMemory(); long used = total - free; - log.info("System report: Used memory: {} MB; Free memory: {} MB; Total memory: {} MB; No. of threads: {}", - used, free, total, Thread.activeCount()); + log.info("Total memory: {}; Used memory: {}; Free memory: {}; Max memory: {}; No. of threads: {}", + Utilities.readableFileSize(total), + Utilities.readableFileSize(used), + Utilities.readableFileSize(free), + Utilities.readableFileSize(runtime.maxMemory()), + Thread.activeCount()); } public static long getUsedMemoryInMB() { diff --git a/common/src/main/java/bisq/common/util/Utilities.java b/common/src/main/java/bisq/common/util/Utilities.java index 6dd3d8de0b..6a47983230 100644 --- a/common/src/main/java/bisq/common/util/Utilities.java +++ b/common/src/main/java/bisq/common/util/Utilities.java @@ -19,12 +19,8 @@ package bisq.common.util; import org.bitcoinj.core.Utils; -import com.google.gson.ExclusionStrategy; -import com.google.gson.FieldAttributes; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - import com.google.common.base.Splitter; +import com.google.common.primitives.Ints; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; @@ -52,9 +48,11 @@ import java.nio.file.Paths; import java.io.File; import java.io.IOException; +import java.util.Arrays; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Random; @@ -85,15 +83,6 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class Utilities { - public static String objectToJson(Object object) { - Gson gson = new GsonBuilder() - .setExclusionStrategies(new AnnotationExclusionStrategy()) - /*.excludeFieldsWithModifiers(Modifier.TRANSIENT)*/ - /* .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)*/ - .setPrettyPrinting() - .create(); - return gson.toJson(object); - } public static ExecutorService getSingleThreadExecutor(String name) { final ThreadFactory threadFactory = new ThreadFactoryBuilder() @@ -337,12 +326,12 @@ public class Utilities { public static void openURI(URI uri) throws IOException { if (!DesktopUtil.browse(uri)) - throw new IOException("Failed to open URI: " + uri.toString()); + throw new IOException("Failed to open URI: " + uri); } public static void openFile(File file) throws IOException { if (!DesktopUtil.open(file)) - throw new IOException("Failed to open file: " + file.toString()); + throw new IOException("Failed to open file: " + file); } public static String getDownloadOfHomeDir() { @@ -447,18 +436,6 @@ public class Utilities { return new File(Utilities.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getPath(); } - private static class AnnotationExclusionStrategy implements ExclusionStrategy { - @Override - public boolean shouldSkipField(FieldAttributes f) { - return f.getAnnotation(JsonExclude.class) != null; - } - - @Override - public boolean shouldSkipClass(Class clazz) { - return false; - } - } - public static String toTruncatedString(Object message) { return toTruncatedString(message, 200, true); } @@ -480,6 +457,16 @@ public class Utilities { } + public static List toListOfWrappedStrings(String s, int wrapLength) { + StringBuilder sb = new StringBuilder(s); + int i = 0; + while (i + wrapLength < sb.length() && (i = sb.lastIndexOf(" ", i + wrapLength)) != -1) { + sb.replace(i, i + 1, "\n"); + } + String[] splitLine = sb.toString().split("\n"); + return Arrays.asList(splitLine); + } + public static String getRandomPrefix(int minLength, int maxLength) { int length = minLength + new Random().nextInt(maxLength - minLength + 1); String result; @@ -539,6 +526,34 @@ public class Utilities { return result; } + public static byte[] copyRightAligned(byte[] src, int newLength) { + byte[] dest = new byte[newLength]; + int srcPos = Math.max(src.length - newLength, 0); + int destPos = Math.max(newLength - src.length, 0); + System.arraycopy(src, srcPos, dest, destPos, newLength - destPos); + return dest; + } + + public static byte[] intsToBytesBE(int[] ints) { + byte[] bytes = new byte[ints.length * 4]; + int i = 0; + for (int v : ints) { + bytes[i++] = (byte) (v >> 24); + bytes[i++] = (byte) (v >> 16); + bytes[i++] = (byte) (v >> 8); + bytes[i++] = (byte) v; + } + return bytes; + } + + public static int[] bytesToIntsBE(byte[] bytes) { + int[] ints = new int[bytes.length / 4]; + for (int i = 0, j = 0; i < bytes.length / 4; i++) { + ints[i] = Ints.fromBytes(bytes[j++], bytes[j++], bytes[j++], bytes[j++]); + } + return ints; + } + // Helper to filter unique elements by key public static Predicate distinctByKey(Function keyExtractor) { Map map = new ConcurrentHashMap<>(); @@ -591,4 +606,8 @@ public class Utilities { } return result; } + + public static String cleanString(String string) { + return string.replaceAll("[\\t\\n\\r]+", " "); + } } diff --git a/common/src/test/java/bisq/common/config/ConfigTests.java b/common/src/test/java/bisq/common/config/ConfigTests.java index fbc4015193..a354f12980 100644 --- a/common/src/test/java/bisq/common/config/ConfigTests.java +++ b/common/src/test/java/bisq/common/config/ConfigTests.java @@ -139,9 +139,9 @@ public class ConfigTests { @Test public void whenConfigFileOptionIsSetToNonExistentFile_thenConfigExceptionIsThrown() { - String filepath = "/no/such/bisq.properties"; + String filepath = "/no/such/haveno.properties"; if (System.getProperty("os.name").startsWith("Windows")) { - filepath = "C:\\no\\such\\bisq.properties"; + filepath = "C:\\no\\such\\haveno.properties"; } exceptionRule.expect(ConfigException.class); exceptionRule.expectMessage(format("The specified config file '%s' does not exist", filepath)); @@ -152,7 +152,7 @@ public class ConfigTests { public void whenConfigFileOptionIsSetInConfigFile_thenConfigExceptionIsThrown() throws IOException { File configFile = File.createTempFile("bisq", "properties"); try (PrintWriter writer = new PrintWriter(configFile)) { - writer.println(new ConfigFileOption(CONFIG_FILE, "/tmp/other.bisq.properties")); + writer.println(new ConfigFileOption(CONFIG_FILE, "/tmp/other.haveno.properties")); } exceptionRule.expect(ConfigException.class); exceptionRule.expectMessage(format("The '%s' option is disallowed in config files", CONFIG_FILE)); diff --git a/common/src/test/java/bisq/common/util/PermutationTest.java b/common/src/test/java/bisq/common/util/PermutationTest.java index 5509835fc2..3ebc3bc1fe 100644 --- a/common/src/test/java/bisq/common/util/PermutationTest.java +++ b/common/src/test/java/bisq/common/util/PermutationTest.java @@ -21,8 +21,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.function.BiFunction; - +import java.util.function.BiPredicate; import org.junit.Test; @@ -108,7 +107,7 @@ public class PermutationTest { List result; List list; List expected; - BiFunction, Boolean> predicate = (target, variationList) -> variationList.toString().equals(target); + BiPredicate> predicate = (target, variationList) -> variationList.toString().equals(target); list = Arrays.asList(a, b, c, d, e); @@ -124,11 +123,11 @@ public class PermutationTest { @Test public void testBreakAtLimit() { - BiFunction, Boolean> predicate = + BiPredicate> predicate = (target, variationList) -> variationList.toString().equals(target); var list = Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o"); var expected = Arrays.asList("b", "g", "m"); - + // Takes around 32508 tries starting from longer strings var limit = 100000; var result = PermutationUtil.findMatchingPermutation(expected.toString(), list, predicate, limit); diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java index 083796febe..1615502105 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java @@ -23,9 +23,8 @@ import bisq.core.filter.FilterManager; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferRestrictions; -import bisq.core.payment.AssetAccount; import bisq.core.payment.ChargeBackRisk; import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentAccountPayload; @@ -34,7 +33,6 @@ import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.arbitration.TraderDataItem; import bisq.core.trade.ArbitratorTrade; -import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.trade.protocol.TradingPeer; import bisq.core.user.User; @@ -132,6 +130,10 @@ public class AccountAgeWitnessService { return String.format(displayString, daysUntilLimitLifted); } + public boolean isLimitLifted() { + return this == PEER_LIMIT_LIFTED || this == PEER_SIGNER || this == ARBITRATOR; + } + } private final KeyRing keyRing; @@ -225,7 +227,7 @@ public class AccountAgeWitnessService { private void republishAllFiatAccounts() { if (user.getPaymentAccounts() != null) user.getPaymentAccounts().stream() - .filter(account -> !(account instanceof AssetAccount)) + .filter(account -> account.getPaymentMethod().isFiat()) .forEach(account -> { AccountAgeWitness myWitness = getMyWitness(account.getPaymentAccountPayload()); // We only publish if the date of our witness is inside the date tolerance. @@ -284,8 +286,12 @@ public class AccountAgeWitnessService { return new AccountAgeWitness(hash, new Date().getTime()); } - Optional findWitness(PaymentAccountPayload paymentAccountPayload, + public Optional findWitness(PaymentAccountPayload paymentAccountPayload, PubKeyRing pubKeyRing) { + if (paymentAccountPayload == null) { + return Optional.empty(); + } + byte[] accountInputDataWithSalt = getAccountInputDataWithSalt(paymentAccountPayload); byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt, pubKeyRing.getSignaturePubKeyBytes())); @@ -415,11 +421,11 @@ public class AccountAgeWitnessService { String currencyCode, AccountAgeWitness accountAgeWitness, AccountAge accountAgeCategory, - OfferPayload.Direction direction, + OfferDirection direction, PaymentMethod paymentMethod) { if (CurrencyUtil.isCryptoCurrency(currencyCode) || !PaymentMethod.hasChargebackRisk(paymentMethod, currencyCode) || - direction == OfferPayload.Direction.SELL) { + direction == OfferDirection.SELL) { return maxTradeLimit.value; } @@ -494,7 +500,7 @@ public class AccountAgeWitnessService { return getAccountAge(getMyWitness(paymentAccountPayload), new Date()); } - public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferPayload.Direction direction) { + public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction) { if (paymentAccount == null) return 0; @@ -558,7 +564,7 @@ public class AccountAgeWitnessService { return false; // Check if the peers trade limit is not less than the trade amount - if (!verifyPeersTradeLimit(trade.getOffer(), trade.getTradeAmount(), peersWitness, peersCurrentDate, + if (!verifyPeersTradeLimit(trade.getOffer(), trade.getAmount(), peersWitness, peersCurrentDate, errorMessageHandler)) { log.error("verifyPeersTradeLimit failed: peersPaymentAccountPayload {}", peersPaymentAccountPayload); return false; @@ -635,13 +641,12 @@ public class AccountAgeWitnessService { ErrorMessageHandler errorMessageHandler) { checkNotNull(offer); final String currencyCode = offer.getCurrencyCode(); - final Coin defaultMaxTradeLimit = PaymentMethod.getPaymentMethodById( - offer.getOfferPayload().getPaymentMethodId()).getMaxTradeLimitAsCoin(currencyCode); + final Coin defaultMaxTradeLimit = offer.getPaymentMethod().getMaxTradeLimitAsCoin(currencyCode); long peersCurrentTradeLimit = defaultMaxTradeLimit.value; if (!hasTradeLimitException(peersWitness)) { final long accountSignAge = getWitnessSignAge(peersWitness, peersCurrentDate); AccountAge accountAgeCategory = getPeersAccountAgeCategory(accountSignAge); - OfferPayload.Direction direction = offer.isMyOffer(keyRing) ? + OfferDirection direction = offer.isMyOffer(keyRing) ? offer.getMirroredDirection() : offer.getDirection(); peersCurrentTradeLimit = getTradeLimit(defaultMaxTradeLimit, currencyCode, peersWitness, accountAgeCategory, direction, offer.getPaymentMethod()); @@ -725,7 +730,7 @@ public class AccountAgeWitnessService { public Optional traderSignAndPublishPeersAccountAgeWitness(Trade trade) { AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null); - Coin tradeAmount = trade.getTradeAmount(); + Coin tradeAmount = trade.getAmount(); checkNotNull(trade.getTradingPeer().getPubKeyRing(), "Peer must have a keyring"); PublicKey peersPubKey = trade.getTradingPeer().getPubKeyRing().getSignaturePubKey(); checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade {}", @@ -767,7 +772,7 @@ public class AccountAgeWitnessService { boolean isFiltered = filterManager.isNodeAddressBanned(dispute.getContract().getBuyerNodeAddress()) || filterManager.isNodeAddressBanned(dispute.getContract().getSellerNodeAddress()) || filterManager.isCurrencyBanned(dispute.getContract().getOfferPayload().getCurrencyCode()) || - filterManager.isPaymentMethodBanned(PaymentMethod.getPaymentMethodById(dispute.getContract().getPaymentMethodId())) || + filterManager.isPaymentMethodBanned(PaymentMethod.getPaymentMethod(dispute.getContract().getPaymentMethodId())) || filterManager.arePeersPaymentAccountDataBanned(dispute.getBuyerPaymentAccountPayload()) || filterManager.arePeersPaymentAccountDataBanned(dispute.getSellerPaymentAccountPayload()) || filterManager.isWitnessSignerPubKeyBanned(Utils.HEX.encode(dispute.getContract().getBuyerPubKeyRing().getSignaturePubKeyBytes())) || @@ -916,7 +921,7 @@ public class AccountAgeWitnessService { return accountIsSigner(myWitness) && !peerHasSignedWitness(trade) && - tradeAmountIsSufficient(trade.getTradeAmount()); + tradeAmountIsSufficient(trade.getAmount()); } public String getSignInfoFromAccount(PaymentAccount paymentAccount) { diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java index 923ff0290b..9747fa1eb7 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java @@ -143,7 +143,7 @@ public class AccountAgeWitnessUtils { } boolean isSignWitnessTrade = accountAgeWitnessService.accountIsSigner(witness) && !accountAgeWitnessService.peerHasSignedWitness(trade) && - accountAgeWitnessService.tradeAmountIsSufficient(trade.getTradeAmount()); + accountAgeWitnessService.tradeAmountIsSufficient(trade.getAmount()); log.info("AccountSigning debug log: " + "\ntradeId: {}" + "\nis buyer: {}" + @@ -164,8 +164,8 @@ public class AccountAgeWitnessUtils { checkingSignTrade, // Following cases added to use same logic as in seller signing check accountAgeWitnessService.accountIsSigner(witness), accountAgeWitnessService.peerHasSignedWitness(trade), - trade.getTradeAmount(), - accountAgeWitnessService.tradeAmountIsSufficient(trade.getTradeAmount()), + trade.getAmount(), + accountAgeWitnessService.tradeAmountIsSufficient(trade.getAmount()), isSignWitnessTrade); } } diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 785fb341f7..3b2b67a6bd 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -25,7 +25,7 @@ import bisq.core.api.model.TxFeeRateInfo; import bisq.core.app.AppStartupState; import bisq.core.monetary.Price; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OpenOffer; import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentMethod; @@ -460,7 +460,7 @@ public class CoreApi { public Offer editOffer(String offerId, String currencyCode, - OfferPayload.Direction direction, + OfferDirection direction, Price price, boolean useMarketBasedPrice, double marketPriceMargin, diff --git a/core/src/main/java/bisq/core/api/CoreDisputesService.java b/core/src/main/java/bisq/core/api/CoreDisputesService.java index 40317b1472..f03630ae87 100644 --- a/core/src/main/java/bisq/core/api/CoreDisputesService.java +++ b/core/src/main/java/bisq/core/api/CoreDisputesService.java @@ -3,7 +3,7 @@ package bisq.core.api; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferDirection; import bisq.core.support.SupportType; import bisq.core.support.dispute.Attachment; import bisq.core.support.dispute.Dispute; @@ -124,7 +124,7 @@ public class CoreDisputesService { trade.getId(), pubKey.hashCode(), // trader id, true, - (offer.getDirection() == OfferPayload.Direction.BUY) == isMaker, + (offer.getDirection() == OfferDirection.BUY) == isMaker, isMaker, pubKey, trade.getDate().getTime(), diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 7e978566d3..ac2f55c29a 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -23,6 +23,7 @@ import bisq.core.monetary.Price; import bisq.core.offer.CreateOfferService; import bisq.core.offer.Offer; import bisq.core.offer.OfferBookService; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferFilter; import bisq.core.offer.OfferFilter.Result; import bisq.core.offer.OfferUtil; @@ -45,7 +46,6 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; @@ -58,8 +58,7 @@ import static bisq.common.util.MathUtils.exactMultiply; import static bisq.common.util.MathUtils.roundDoubleToLong; import static bisq.common.util.MathUtils.scaleUpByPowerOf10; import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; -import static bisq.core.offer.OfferPayload.Direction; -import static bisq.core.offer.OfferPayload.Direction.BUY; +import static bisq.core.offer.OfferDirection.BUY; import static bisq.core.payment.PaymentAccountUtil.isPaymentAccountValidForOffer; import static java.lang.String.format; import static java.util.Comparator.comparing; @@ -232,7 +231,7 @@ class CoreOffersService { String upperCaseCurrencyCode = currencyCode.toUpperCase(); String offerId = createOfferService.getRandomOfferId(); - Direction direction = Direction.valueOf(directionAsString.toUpperCase()); + OfferDirection direction = OfferDirection.valueOf(directionAsString.toUpperCase()); Price price = Price.valueOf(upperCaseCurrencyCode, priceStringToLong(priceAsString, upperCaseCurrencyCode)); Coin amount = Coin.valueOf(amountAsLong); Coin minAmount = Coin.valueOf(minAmountAsLong); @@ -264,7 +263,7 @@ class CoreOffersService { // Edit a placed offer. Offer editOffer(String offerId, String currencyCode, - Direction direction, + OfferDirection direction, Price price, boolean useMarketBasedPrice, double marketPriceMargin, diff --git a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java index a9c9d44c0c..77a647d8ab 100644 --- a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java @@ -83,7 +83,7 @@ class CorePaymentAccountsService { List getFiatPaymentMethods() { return PaymentMethod.getPaymentMethods().stream() - .filter(paymentMethod -> !paymentMethod.isAsset()) + .filter(paymentMethod -> !paymentMethod.isBlockchain()) .sorted(Comparator.comparing(PaymentMethod::getId)) .collect(Collectors.toList()); } @@ -129,15 +129,14 @@ class CorePaymentAccountsService { List getCryptoCurrencyPaymentMethods() { return PaymentMethod.getPaymentMethods().stream() - .filter(PaymentMethod::isAsset) + .filter(PaymentMethod::isAltcoin) .sorted(Comparator.comparing(PaymentMethod::getId)) .collect(Collectors.toList()); } private void verifyPaymentAccountHasRequiredFields(PaymentAccount paymentAccount) { - // Do checks here to make sure required fields are populated. - if (paymentAccount.isTransferwiseAccount() && paymentAccount.getTradeCurrencies().isEmpty()) - throw new IllegalArgumentException(format("no trade currencies defined for %s payment account", + if (!paymentAccount.hasMultipleCurrencies() && paymentAccount.getSingleTradeCurrency() == null) + throw new IllegalArgumentException(format("no trade currency defined for %s payment account", paymentAccount.getPaymentMethod().getDisplayString().toLowerCase())); } } diff --git a/core/src/main/java/bisq/core/api/CorePriceService.java b/core/src/main/java/bisq/core/api/CorePriceService.java index 261f2644dc..00d4457a54 100644 --- a/core/src/main/java/bisq/core/api/CorePriceService.java +++ b/core/src/main/java/bisq/core/api/CorePriceService.java @@ -23,7 +23,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.monetary.Price; import bisq.core.offer.Offer; import bisq.core.offer.OfferBookService; -import bisq.core.offer.OfferPayload.Direction; +import bisq.core.offer.OfferDirection; import bisq.core.provider.price.PriceFeedService; import javax.inject.Inject; @@ -102,8 +102,8 @@ class CorePriceService { var sellOfferSortComparator = offerPriceComparator .thenComparing(offerAmountComparator); - List buyOffers = offerBookService.getOffersByCurrency(Direction.BUY.name(), currencyCode).stream().sorted(buyOfferSortComparator).collect(Collectors.toList()); - List sellOffers = offerBookService.getOffersByCurrency(Direction.SELL.name(), currencyCode).stream().sorted(sellOfferSortComparator).collect(Collectors.toList()); + List buyOffers = offerBookService.getOffersByCurrency(OfferDirection.BUY.name(), currencyCode).stream().sorted(buyOfferSortComparator).collect(Collectors.toList()); + List sellOffers = offerBookService.getOffersByCurrency(OfferDirection.SELL.name(), currencyCode).stream().sorted(sellOfferSortComparator).collect(Collectors.toList()); // Create buyer hashmap {key:price, value:count}, uses LinkedHashMap to maintain insertion order double accumulatedAmount = 0; diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index 1726329884..c8ed902484 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -20,17 +20,15 @@ package bisq.core.api; import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.Offer; -import bisq.core.offer.OfferUtil; import bisq.core.offer.takeoffer.TakeOfferModel; -import bisq.core.support.dispute.Dispute; import bisq.core.support.messages.ChatMessage; import bisq.core.support.traderchat.TradeChatSession; import bisq.core.support.traderchat.TraderChatManager; +import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.Tradable; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; import bisq.core.trade.TradeUtil; -import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.protocol.BuyerProtocol; import bisq.core.trade.protocol.SellerProtocol; import bisq.core.user.User; diff --git a/core/src/main/java/bisq/core/api/model/MarketDepthInfo.java b/core/src/main/java/bisq/core/api/model/MarketDepthInfo.java index c986c8b796..23fd2b9511 100644 --- a/core/src/main/java/bisq/core/api/model/MarketDepthInfo.java +++ b/core/src/main/java/bisq/core/api/model/MarketDepthInfo.java @@ -1,18 +1,18 @@ /* - * This file is part of Bisq. + * This file is part of Haveno. * - * Bisq is free software: you can redistribute it and/or modify it + * 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. * - * Bisq is distributed in the hope that it will be useful, but WITHOUT + * 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 Bisq. If not, see . + * along with Haveno. If not, see . */ package bisq.core.api.model; diff --git a/core/src/main/java/bisq/core/api/model/OfferInfo.java b/core/src/main/java/bisq/core/api/model/OfferInfo.java index 96ca5da45f..88ec23ab63 100644 --- a/core/src/main/java/bisq/core/api/model/OfferInfo.java +++ b/core/src/main/java/bisq/core/api/model/OfferInfo.java @@ -17,17 +17,23 @@ package bisq.core.api.model; +import bisq.core.api.model.builder.OfferInfoBuilder; +import bisq.core.monetary.Price; import bisq.core.offer.Offer; import bisq.core.offer.OpenOffer; import bisq.common.Payload; import bisq.common.proto.ProtoUtil; -import java.util.Objects; import java.util.Optional; import javax.annotation.Nullable; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; +import static bisq.common.util.MathUtils.exactMultiply; +import static bisq.core.util.PriceUtil.reformatMarketPrice; +import static bisq.core.util.VolumeUtil.formatVolume; +import static java.util.Objects.requireNonNull; + @EqualsAndHashCode @ToString @Getter @@ -39,20 +45,20 @@ public class OfferInfo implements Payload { private final String id; private final String direction; - private final long price; + private final String price; private final boolean useMarketBasedPrice; - private final double marketPriceMargin; + private final double marketPriceMarginPct; private final long amount; private final long minAmount; - private final long volume; - private final long minVolume; + private final String volume; + private final String minVolume; private final long txFee; private final long makerFee; @Nullable private final String offerFeePaymentTxId; private final long buyerSecurityDeposit; private final long sellerSecurityDeposit; - private final long triggerPrice; + private final String triggerPrice; private final String paymentAccountId; private final String paymentMethodId; private final String paymentMethodShortName; @@ -62,58 +68,89 @@ public class OfferInfo implements Payload { private final String counterCurrencyCode; private final long date; private final String state; - + private final boolean isActivated; + private final boolean isMyOffer; + private final String ownerNodeAddress; + private final String pubKeyRing; + private final String versionNumber; + private final int protocolVersion; public OfferInfo(OfferInfoBuilder builder) { - this.id = builder.id; - this.direction = builder.direction; - this.price = builder.price; - this.useMarketBasedPrice = builder.useMarketBasedPrice; - this.marketPriceMargin = builder.marketPriceMargin; - this.amount = builder.amount; - this.minAmount = builder.minAmount; - this.volume = builder.volume; - this.minVolume = builder.minVolume; - this.txFee = builder.txFee; - this.makerFee = builder.makerFee; - this.offerFeePaymentTxId = builder.offerFeePaymentTxId; - this.buyerSecurityDeposit = builder.buyerSecurityDeposit; - this.sellerSecurityDeposit = builder.sellerSecurityDeposit; - this.triggerPrice = builder.triggerPrice; - this.paymentAccountId = builder.paymentAccountId; - this.paymentMethodId = builder.paymentMethodId; - this.paymentMethodShortName = builder.paymentMethodShortName; - this.baseCurrencyCode = builder.baseCurrencyCode; - this.counterCurrencyCode = builder.counterCurrencyCode; - this.date = builder.date; - this.state = builder.state; - + this.id = builder.getId(); + this.direction = builder.getDirection(); + this.price = builder.getPrice(); + this.useMarketBasedPrice = builder.isUseMarketBasedPrice(); + this.marketPriceMarginPct = builder.getMarketPriceMarginPct(); + this.amount = builder.getAmount(); + this.minAmount = builder.getMinAmount(); + this.volume = builder.getVolume(); + this.minVolume = builder.getMinVolume(); + this.txFee = builder.getTxFee(); + this.makerFee = builder.getMakerFee(); + this.offerFeePaymentTxId = builder.getOfferFeePaymentTxId(); + this.buyerSecurityDeposit = builder.getBuyerSecurityDeposit(); + this.sellerSecurityDeposit = builder.getSellerSecurityDeposit(); + this.triggerPrice = builder.getTriggerPrice(); + this.paymentAccountId = builder.getPaymentAccountId(); + this.paymentMethodId = builder.getPaymentMethodId(); + this.paymentMethodShortName = builder.getPaymentMethodShortName(); + this.baseCurrencyCode = builder.getBaseCurrencyCode(); + this.counterCurrencyCode = builder.getCounterCurrencyCode(); + this.date = builder.getDate(); + this.state = builder.getState(); + this.isActivated = builder.isActivated(); + this.isMyOffer = builder.isMyOffer(); + this.ownerNodeAddress = builder.getOwnerNodeAddress(); + this.pubKeyRing = builder.getPubKeyRing(); + this.versionNumber = builder.getVersionNumber(); + this.protocolVersion = builder.getProtocolVersion(); } public static OfferInfo toOfferInfo(Offer offer) { - return getOfferInfoBuilder(offer).build(); + return getBuilder(offer) + .withIsMyOffer(false) + .withIsActivated(true) + .build(); } - public static OfferInfo toOfferInfo(Offer offer, OpenOffer openOffer) { - OfferInfoBuilder builder = getOfferInfoBuilder(offer); - if (openOffer != null) { - builder.withState(openOffer.getState().name()); - builder.withTriggerPrice(openOffer.getTriggerPrice()); - } - return builder.build(); + public static OfferInfo toMyOfferInfo(OpenOffer openOffer) { + // An OpenOffer is always my offer. + var offer = openOffer.getOffer(); + var currencyCode = offer.getCurrencyCode(); + var isActivated = !openOffer.isDeactivated(); + Optional optionalTriggerPrice = openOffer.getTriggerPrice() > 0 + ? Optional.of(Price.valueOf(currencyCode, openOffer.getTriggerPrice())) + : Optional.empty(); + var preciseTriggerPrice = optionalTriggerPrice + .map(value -> reformatMarketPrice(value.toPlainString(), currencyCode)) + .orElse("0"); + return getBuilder(offer) + .withTriggerPrice(preciseTriggerPrice) + .withState(openOffer.getState().name()) + .withIsActivated(isActivated) + .build(); } - private static OfferInfoBuilder getOfferInfoBuilder(Offer offer) { + private static OfferInfoBuilder getBuilder(Offer offer) { + // OfferInfo protos are passed to API client, and some field + // values are converted to displayable, unambiguous form. + var currencyCode = offer.getCurrencyCode(); + var preciseOfferPrice = reformatMarketPrice( + requireNonNull(offer.getPrice()).toPlainString(), + currencyCode); + var marketPriceMarginAsPctLiteral = exactMultiply(offer.getMarketPriceMarginPct(), 100); + var roundedVolume = formatVolume(requireNonNull(offer.getVolume())); + var roundedMinVolume = formatVolume(requireNonNull(offer.getMinVolume())); return new OfferInfoBuilder() .withId(offer.getId()) .withDirection(offer.getDirection().name()) - .withPrice(Objects.requireNonNull(offer.getPrice()).getValue()) + .withPrice(preciseOfferPrice) .withUseMarketBasedPrice(offer.isUseMarketBasedPrice()) - .withMarketPriceMargin(offer.getMarketPriceMargin()) + .withMarketPriceMarginPct(marketPriceMarginAsPctLiteral) .withAmount(offer.getAmount().value) .withMinAmount(offer.getMinAmount().value) - .withVolume(Objects.requireNonNull(offer.getVolume()).getValue()) - .withMinVolume(Objects.requireNonNull(offer.getMinVolume()).getValue()) + .withVolume(roundedVolume) + .withMinVolume(roundedMinVolume) .withMakerFee(offer.getMakerFee().value) .withTxFee(offer.getTxFee().value) .withOfferFeePaymentTxId(offer.getOfferFeePaymentTxId()) @@ -122,10 +159,14 @@ public class OfferInfo implements Payload { .withPaymentAccountId(offer.getMakerPaymentAccountId()) .withPaymentMethodId(offer.getPaymentMethod().getId()) .withPaymentMethodShortName(offer.getPaymentMethod().getShortName()) - .withBaseCurrencyCode(offer.getOfferPayload().getBaseCurrencyCode()) - .withCounterCurrencyCode(offer.getOfferPayload().getCounterCurrencyCode()) + .withBaseCurrencyCode(offer.getBaseCurrencyCode()) + .withCounterCurrencyCode(offer.getCounterCurrencyCode()) .withDate(offer.getDate().getTime()) - .withState(offer.getState().name()); + .withState(offer.getState().name()) + .withOwnerNodeAddress(offer.getOfferPayload().getOwnerNodeAddress().getFullAddress()) + .withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString()) + .withVersionNumber(offer.getOfferPayload().getVersionNr()) + .withProtocolVersion(offer.getOfferPayload().getProtocolVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -139,7 +180,7 @@ public class OfferInfo implements Payload { .setDirection(direction) .setPrice(price) .setUseMarketBasedPrice(useMarketBasedPrice) - .setMarketPriceMargin(marketPriceMargin) + .setMarketPriceMarginPct(marketPriceMarginPct) .setAmount(amount) .setMinAmount(minAmount) .setVolume(volume) @@ -148,14 +189,20 @@ public class OfferInfo implements Payload { .setTxFee(txFee) .setBuyerSecurityDeposit(buyerSecurityDeposit) .setSellerSecurityDeposit(sellerSecurityDeposit) - .setTriggerPrice(triggerPrice) + .setTriggerPrice(triggerPrice == null ? "0" : triggerPrice) .setPaymentAccountId(paymentAccountId) .setPaymentMethodId(paymentMethodId) .setPaymentMethodShortName(paymentMethodShortName) .setBaseCurrencyCode(baseCurrencyCode) .setCounterCurrencyCode(counterCurrencyCode) .setDate(date) - .setState(state); + .setState(state) + .setIsActivated(isActivated) + .setIsMyOffer(isMyOffer) + .setOwnerNodeAddress(ownerNodeAddress) + .setPubKeyRing(pubKeyRing) + .setVersionNr(versionNumber) + .setProtocolVersion(protocolVersion); Optional.ofNullable(offerFeePaymentTxId).ifPresent(builder::setOfferFeePaymentTxId); return builder.build(); } @@ -167,7 +214,7 @@ public class OfferInfo implements Payload { .withDirection(proto.getDirection()) .withPrice(proto.getPrice()) .withUseMarketBasedPrice(proto.getUseMarketBasedPrice()) - .withMarketPriceMargin(proto.getMarketPriceMargin()) + .withMarketPriceMarginPct(proto.getMarketPriceMarginPct()) .withAmount(proto.getAmount()) .withMinAmount(proto.getMinAmount()) .withVolume(proto.getVolume()) @@ -185,152 +232,12 @@ public class OfferInfo implements Payload { .withCounterCurrencyCode(proto.getCounterCurrencyCode()) .withDate(proto.getDate()) .withState(proto.getState()) + .withIsActivated(proto.getIsActivated()) + .withIsMyOffer(proto.getIsMyOffer()) + .withOwnerNodeAddress(proto.getOwnerNodeAddress()) + .withPubKeyRing(proto.getPubKeyRing()) + .withVersionNumber(proto.getVersionNr()) + .withProtocolVersion(proto.getProtocolVersion()) .build(); } - - /* - * OfferInfoBuilder helps avoid bungling use of a large OfferInfo constructor - * argument list. If consecutive argument values of the same type are not - * ordered correctly, the compiler won't complain but the resulting bugs could - * be hard to find and fix. - */ - public static class OfferInfoBuilder { - private String id; - private String direction; - private long price; - private boolean useMarketBasedPrice; - private double marketPriceMargin; - private long amount; - private long minAmount; - private long volume; - private long minVolume; - private long txFee; - private long makerFee; - private String offerFeePaymentTxId; - private long buyerSecurityDeposit; - private long sellerSecurityDeposit; - private long triggerPrice; - private String paymentAccountId; - private String paymentMethodId; - private String paymentMethodShortName; - private String baseCurrencyCode; - private String counterCurrencyCode; - private long date; - private String state; - - public OfferInfoBuilder withId(String id) { - this.id = id; - return this; - } - - public OfferInfoBuilder withDirection(String direction) { - this.direction = direction; - return this; - } - - public OfferInfoBuilder withPrice(long price) { - this.price = price; - return this; - } - - public OfferInfoBuilder withUseMarketBasedPrice(boolean useMarketBasedPrice) { - this.useMarketBasedPrice = useMarketBasedPrice; - return this; - } - - public OfferInfoBuilder withMarketPriceMargin(double useMarketBasedPrice) { - this.marketPriceMargin = useMarketBasedPrice; - return this; - } - - public OfferInfoBuilder withAmount(long amount) { - this.amount = amount; - return this; - } - - public OfferInfoBuilder withMinAmount(long minAmount) { - this.minAmount = minAmount; - return this; - } - - public OfferInfoBuilder withVolume(long volume) { - this.volume = volume; - return this; - } - - public OfferInfoBuilder withMinVolume(long minVolume) { - this.minVolume = minVolume; - return this; - } - - public OfferInfoBuilder withTxFee(long txFee) { - this.txFee = txFee; - return this; - } - - public OfferInfoBuilder withMakerFee(long makerFee) { - this.makerFee = makerFee; - return this; - } - - public OfferInfoBuilder withOfferFeePaymentTxId(String offerFeePaymentTxId) { - this.offerFeePaymentTxId = offerFeePaymentTxId; - return this; - } - - public OfferInfoBuilder withBuyerSecurityDeposit(long buyerSecurityDeposit) { - this.buyerSecurityDeposit = buyerSecurityDeposit; - return this; - } - - public OfferInfoBuilder withSellerSecurityDeposit(long sellerSecurityDeposit) { - this.sellerSecurityDeposit = sellerSecurityDeposit; - return this; - } - - public OfferInfoBuilder withTriggerPrice(long triggerPrice) { - this.triggerPrice = triggerPrice; - return this; - } - - - public OfferInfoBuilder withPaymentAccountId(String paymentAccountId) { - this.paymentAccountId = paymentAccountId; - return this; - } - - public OfferInfoBuilder withPaymentMethodId(String paymentMethodId) { - this.paymentMethodId = paymentMethodId; - return this; - } - - public OfferInfoBuilder withPaymentMethodShortName(String paymentMethodShortName) { - this.paymentMethodShortName = paymentMethodShortName; - return this; - } - - public OfferInfoBuilder withBaseCurrencyCode(String baseCurrencyCode) { - this.baseCurrencyCode = baseCurrencyCode; - return this; - } - - public OfferInfoBuilder withCounterCurrencyCode(String counterCurrencyCode) { - this.counterCurrencyCode = counterCurrencyCode; - return this; - } - - public OfferInfoBuilder withDate(long date) { - this.date = date; - return this; - } - - public OfferInfoBuilder withState(String state) { - this.state = state; - return this; - } - - public OfferInfo build() { - return new OfferInfo(this); - } - } } diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java index 1ba9b03a17..a5aeae247d 100644 --- a/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java +++ b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java @@ -47,7 +47,7 @@ import java.lang.reflect.Type; import lombok.extern.slf4j.Slf4j; -import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById; +import static bisq.core.payment.payload.PaymentMethod.getPaymentMethod; import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.String.format; import static java.lang.System.getProperty; @@ -134,11 +134,12 @@ public class PaymentAccountForm { "maxTradePeriod", "paymentAccountPayload", "paymentMethod", - "paymentMethodId", // This field will be included, but handled differently. - "selectedTradeCurrency", - "tradeCurrencies", // This field may be included, but handled differently. + "paymentMethodId", // Will be included, but handled differently. + "persistedAccountName", // Automatically set in PaymentAccount.onPersistChanges(). + "selectedTradeCurrency", // May be included, but handled differently. + "tradeCurrencies", // May be included, but handled differently. "HOLDER_NAME", - "SALT" // This field will be included, but handled differently. + "SALT" // Will be included, but handled differently. }; /** @@ -148,7 +149,7 @@ public class PaymentAccountForm { * @return A uniquely named tmp file used to define new payment account details. */ public File getPaymentAccountForm(String paymentMethodId) { - PaymentMethod paymentMethod = getPaymentMethodById(paymentMethodId); + PaymentMethod paymentMethod = getPaymentMethod(paymentMethodId); File file = getTmpJsonFile(paymentMethodId); try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(checkNotNull(file), false), UTF_8)) { PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod); @@ -244,7 +245,7 @@ public class PaymentAccountForm { } private Class getPaymentAccountClass(String paymentMethodId) { - PaymentMethod paymentMethod = getPaymentMethodById(paymentMethodId); + PaymentMethod paymentMethod = getPaymentMethod(paymentMethodId); return PaymentAccountFactory.getPaymentAccount(paymentMethod).getClass(); } } diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java b/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java index 507abdb9f9..631b6f4c9f 100644 --- a/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java +++ b/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java @@ -20,6 +20,7 @@ package bisq.core.api.model; import bisq.core.locale.Country; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.payment.CountryBasedPaymentAccount; import bisq.core.payment.MoneyGramAccount; @@ -31,8 +32,6 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; -import org.apache.commons.lang3.StringUtils; - import java.io.IOException; import java.util.ArrayList; @@ -53,10 +52,10 @@ import lombok.extern.slf4j.Slf4j; import static bisq.common.util.ReflectionUtils.*; import static bisq.common.util.Utilities.decodeFromHex; import static bisq.core.locale.CountryUtil.findCountryByCode; -import static bisq.core.locale.CurrencyUtil.getAllTransferwiseCurrencies; import static bisq.core.locale.CurrencyUtil.getCurrencyByCountryCode; -import static bisq.core.locale.CurrencyUtil.getTradeCurrencies; import static bisq.core.locale.CurrencyUtil.getTradeCurrenciesInList; +import static bisq.core.locale.CurrencyUtil.getTradeCurrency; +import static bisq.core.payment.payload.PaymentMethod.MONEY_GRAM_ID; import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.String.format; import static java.util.Arrays.stream; @@ -64,6 +63,7 @@ import static java.util.Collections.singletonList; import static java.util.Collections.unmodifiableMap; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; +import static org.apache.commons.lang3.StringUtils.capitalize; @Slf4j class PaymentAccountTypeAdapter extends TypeAdapter { @@ -110,13 +110,7 @@ class PaymentAccountTypeAdapter extends TypeAdapter { // We're not serializing a real payment account instance here. out.beginObject(); - // All json forms start with immutable _COMMENTS_ and paymentMethodId fields. - out.name("_COMMENTS_"); - out.beginArray(); - for (String s : JSON_COMMENTS) { - out.value(s); - } - out.endArray(); + writeComments(out, account); out.name("paymentMethodId"); out.value(account.getPaymentMethod().getId()); @@ -131,9 +125,29 @@ class PaymentAccountTypeAdapter extends TypeAdapter { out.endObject(); } + private void writeComments(JsonWriter out, PaymentAccount account) throws IOException { + // All json forms start with immutable _COMMENTS_ and paymentMethodId fields. + out.name("_COMMENTS_"); + out.beginArray(); + for (String s : JSON_COMMENTS) { + out.value(s); + } + if (account.hasPaymentMethodWithId("SWIFT_ID")) { + // Add extra comments for more complex swift account form. + List wrappedSwiftComments = Res.getWrappedAsList("payment.swift.info.account", 110); + for (String line : wrappedSwiftComments) { + out.value(line); + } + } + out.endArray(); + } + + private void writeInnerMutableFields(JsonWriter out, PaymentAccount account) { - if (account.isTransferwiseAccount()) + if (account.hasMultipleCurrencies()) { writeTradeCurrenciesField(out, account); + writeSelectedTradeCurrencyField(out, account); + } fieldSettersMap.forEach((field, value) -> { try { @@ -155,7 +169,7 @@ class PaymentAccountTypeAdapter extends TypeAdapter { } catch (Exception ex) { String errMsg = format("cannot create a new %s json form", account.getClass().getSimpleName()); - log.error(StringUtils.capitalize(errMsg) + ".", ex); + log.error(capitalize(errMsg) + ".", ex); throw new IllegalStateException("programmer error: " + errMsg); } }); @@ -170,15 +184,31 @@ class PaymentAccountTypeAdapter extends TypeAdapter { String fieldName = "tradeCurrencies"; log.debug("Append form with non-settable field: {}", fieldName); out.name(fieldName); - out.value("comma delimited currency code list, e.g., gbp,eur"); + out.value("comma delimited currency code list, e.g., gbp,eur,jpy,usd"); } catch (Exception ex) { String errMsg = format("cannot create a new %s json form", account.getClass().getSimpleName()); - log.error(StringUtils.capitalize(errMsg) + ".", ex); + log.error(capitalize(errMsg) + ".", ex); throw new IllegalStateException("programmer error: " + errMsg); } } + // PaymentAccounts that support multiple 'tradeCurrencies' need to define a + // 'selectedTradeCurrency' field (not simply defaulting to first in list). + // Write this field to the form. + private void writeSelectedTradeCurrencyField(JsonWriter out, PaymentAccount account) { + try { + String fieldName = "selectedTradeCurrency"; + log.debug("Append form with settable field: {}", fieldName); + out.name(fieldName); + out.value("primary trading currency code, e.g., eur"); + } catch (Exception ex) { + String errMsg = format("cannot create a new %s json form", + account.getClass().getSimpleName()); + log.error(capitalize(errMsg) + ".", ex); + throw new IllegalStateException("programmer error: " + errMsg); + } + } @Override public PaymentAccount read(JsonReader in) throws IOException { @@ -187,12 +217,17 @@ class PaymentAccountTypeAdapter extends TypeAdapter { while (in.hasNext()) { String currentFieldName = in.nextName(); - // The tradeCurrency field is common to all payment account types, + // The tradeCurrencies field is common to all payment account types, // but has no setter. if (didReadTradeCurrenciesField(in, account, currentFieldName)) continue; - // Some of the fields are common to all payment account types. + // The selectedTradeCurrency field is common to all payment account types, + // but is @Nullable, and may not need to be explicitly defined by user. + if (didReadSelectedTradeCurrencyField(in, account, currentFieldName)) + continue; + + // Some fields are common to all payment account types. if (didReadCommonField(in, account, currentFieldName)) continue; @@ -283,7 +318,7 @@ class PaymentAccountTypeAdapter extends TypeAdapter { } } catch (IOException ex) { String errMsg = "cannot see next string in json reader"; - log.error(StringUtils.capitalize(errMsg) + ".", ex); + log.error(capitalize(errMsg) + ".", ex); throw new IllegalStateException("programmer error: " + errMsg); } } @@ -299,7 +334,7 @@ class PaymentAccountTypeAdapter extends TypeAdapter { } } catch (IOException ex) { String errMsg = "cannot see next long in json reader"; - log.error(StringUtils.capitalize(errMsg) + ".", ex); + log.error(capitalize(errMsg) + ".", ex); throw new IllegalStateException("programmer error: " + errMsg); } } @@ -317,37 +352,55 @@ class PaymentAccountTypeAdapter extends TypeAdapter { private boolean didReadTradeCurrenciesField(JsonReader in, PaymentAccount account, String fieldName) { + if (!fieldName.equals("tradeCurrencies")) + return false; + // The PaymentAccount.tradeCurrencies field is a special case because it has - // no setter, and we add currencies to the List here. Normally, it is an - // excluded field, TransferwiseAccount excepted. - if (fieldName.equals("tradeCurrencies")) { - String fieldValue = nextStringOrNull(in); - List currencyCodes = commaDelimitedCodesToList.apply(fieldValue); - - Optional> tradeCurrencies; - if (account.isTransferwiseAccount()) - tradeCurrencies = getTradeCurrenciesInList(currencyCodes, getAllTransferwiseCurrencies()); - else - tradeCurrencies = getTradeCurrencies(currencyCodes); - - if (tradeCurrencies.isPresent()) { - for (TradeCurrency tradeCurrency : tradeCurrencies.get()) { - account.addCurrency(tradeCurrency); - } - // For api users, define a selected currency. - account.setSelectedTradeCurrency(account.getTradeCurrency().orElse(null)); - } else { - // Log a warning. We should not throw an exception here because the - // gson library will not pass it up to the calling Bisq class as it - // would be defined here. Do a check in a calling class to make sure - // the tradeCurrencies field is populated in the PaymentAccount - // object, if it is required for the payment account method. - log.warn("No trade currencies were found in the {} account form.", - account.getPaymentMethod().getDisplayString()); + // no setter, so we add currencies to the List here if the payment account + // supports multiple trade currencies. + String fieldValue = nextStringOrNull(in); + List currencyCodes = commaDelimitedCodesToList.apply(fieldValue); + Optional> tradeCurrencies = getReconciledTradeCurrencies(currencyCodes, account); + if (tradeCurrencies.isPresent()) { + for (TradeCurrency tradeCurrency : tradeCurrencies.get()) { + account.addCurrency(tradeCurrency); } - return true; + } else { + // Log a warning. We should not throw an exception here because the + // gson library will not pass it up to the calling Bisq object exactly as + // it would be defined here (causing confusion). Do a check in a calling + // class to make sure the tradeCurrencies field is populated in the + // PaymentAccount object, if it is required for the payment account method. + log.warn("No trade currencies were found in the {} account form.", + account.getPaymentMethod().getDisplayString()); } - return false; + return true; + } + + private Optional> getReconciledTradeCurrencies(List currencyCodes, + PaymentAccount account) { + return getTradeCurrenciesInList(currencyCodes, account.getSupportedCurrencies()); + } + + private boolean didReadSelectedTradeCurrencyField(JsonReader in, + PaymentAccount account, + String fieldName) { + if (!fieldName.equals("selectedTradeCurrency")) + return false; + + String fieldValue = nextStringOrNull(in); + if (fieldValue != null && !fieldValue.isEmpty()) { + Optional tradeCurrency = getTradeCurrency(fieldValue.toUpperCase()); + if (tradeCurrency.isPresent()) { + account.setSelectedTradeCurrency(tradeCurrency.get()); + } else { + // Log an error. We should not throw an exception here because the + // gson library will not pass it up to the calling Bisq object exactly as + // it would be defined here (causing confusion). + log.error("{} is not a valid trade currency code.", fieldValue); + } + } + return true; } private boolean didReadCommonField(JsonReader in, @@ -356,8 +409,8 @@ class PaymentAccountTypeAdapter extends TypeAdapter { switch (fieldName) { case "_COMMENTS_": case "paymentMethodId": - // Skip over the the comments and paymentMethodId, which is already - // set on the PaymentAccount instance. + // Skip over comments and paymentMethodId field, which + // are already set on the PaymentAccount instance. in.skipValue(); return true; case "accountName": @@ -388,12 +441,12 @@ class PaymentAccountTypeAdapter extends TypeAdapter { ((CountryBasedPaymentAccount) account).setCountry(country.get()); FiatCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode)); account.setSingleTradeCurrency(fiatCurrency); - } else if (account.isMoneyGramAccount()) { + } else if (account.hasPaymentMethodWithId(MONEY_GRAM_ID)) { ((MoneyGramAccount) account).setCountry(country.get()); } else { String errMsg = format("cannot set the country on a %s", paymentAccountType.getSimpleName()); - log.error(StringUtils.capitalize(errMsg) + "."); + log.error(capitalize(errMsg) + "."); throw new IllegalStateException("programmer error: " + errMsg); } @@ -414,7 +467,7 @@ class PaymentAccountTypeAdapter extends TypeAdapter { } catch (Exception ex) { String errMsg = format("cannot get the payload class for %s", paymentAccountType.getSimpleName()); - log.error(StringUtils.capitalize(errMsg) + ".", ex); + log.error(capitalize(errMsg) + ".", ex); throw new IllegalStateException("programmer error: " + errMsg); } } @@ -431,7 +484,7 @@ class PaymentAccountTypeAdapter extends TypeAdapter { | InvocationTargetException ex) { String errMsg = format("cannot instantiate a new %s", paymentAccountType.getSimpleName()); - log.error(StringUtils.capitalize(errMsg) + ".", ex); + log.error(capitalize(errMsg) + ".", ex); throw new IllegalStateException("programmer error: " + errMsg); } } diff --git a/core/src/main/java/bisq/core/api/model/TradeInfo.java b/core/src/main/java/bisq/core/api/model/TradeInfo.java index c8d2d1e7b5..fc656a19d5 100644 --- a/core/src/main/java/bisq/core/api/model/TradeInfo.java +++ b/core/src/main/java/bisq/core/api/model/TradeInfo.java @@ -17,18 +17,21 @@ package bisq.core.api.model; -import bisq.core.trade.Contract; +import bisq.core.api.model.builder.TradeInfoV1Builder; import bisq.core.trade.Trade; +import bisq.core.trade.Contract; import bisq.common.Payload; -import java.util.Objects; - +import java.util.function.Function; import lombok.EqualsAndHashCode; import lombok.Getter; import static bisq.core.api.model.OfferInfo.toOfferInfo; import static bisq.core.api.model.PaymentAccountPayloadInfo.toPaymentAccountPayloadInfo; +import static bisq.core.util.PriceUtil.reformatMarketPrice; +import static bisq.core.util.VolumeUtil.formatVolume; +import static java.util.Objects.requireNonNull; @EqualsAndHashCode @Getter @@ -38,6 +41,21 @@ public class TradeInfo implements Payload { // lighter weight TradeInfo proto wrapper instead, containing just enough fields to // view and interact with trades. + private static final Function toPeerNodeAddress = (trade) -> + trade.getTradingPeerNodeAddress() == null + ? "" + : trade.getTradingPeerNodeAddress().getFullAddress(); + + private static final Function toRoundedVolume = (trade) -> + trade.getVolume() == null + ? "" + : formatVolume(requireNonNull(trade.getVolume())); + + private static final Function toPreciseTradePrice = (trade) -> + reformatMarketPrice(requireNonNull(trade.getPrice()).toPlainString(), + trade.getOffer().getCurrencyCode()); + + // Bisq v1 trade protocol fields (some are in common with the BSQ Swap protocol). private final OfferInfo offer; private final String tradeId; private final String shortId; @@ -49,47 +67,49 @@ public class TradeInfo implements Payload { private final String makerDepositTxId; private final String takerDepositTxId; private final String payoutTxId; - private final long tradeAmountAsLong; - private final long tradePrice; + private final long amountAsLong; + private final String price; + private final String volume; private final String tradingPeerNodeAddress; private final String state; private final String phase; - private final String tradePeriodState; + private final String periodState; private final boolean isDepositPublished; private final boolean isDepositUnlocked; private final boolean isPaymentSent; private final boolean isPaymentReceived; private final boolean isPayoutPublished; - private final boolean isWithdrawn; + private final boolean isCompleted; private final String contractAsJson; private final ContractInfo contract; - public TradeInfo(TradeInfoBuilder builder) { - this.offer = builder.offer; - this.tradeId = builder.tradeId; - this.shortId = builder.shortId; - this.date = builder.date; - this.role = builder.role; - this.txFeeAsLong = builder.txFeeAsLong; - this.takerFeeAsLong = builder.takerFeeAsLong; - this.takerFeeTxId = builder.takerFeeTxId; - this.makerDepositTxId = builder.makerDepositTxId; - this.takerDepositTxId = builder.takerDepositTxId; - this.payoutTxId = builder.payoutTxId; - this.tradeAmountAsLong = builder.tradeAmountAsLong; - this.tradePrice = builder.tradePrice; - this.tradingPeerNodeAddress = builder.tradingPeerNodeAddress; - this.state = builder.state; - this.phase = builder.phase; - this.tradePeriodState = builder.tradePeriodState; - this.isDepositPublished = builder.isDepositPublished; - this.isDepositUnlocked = builder.isDepositConfirmed; - this.isPaymentSent = builder.isPaymentSent; - this.isPaymentReceived = builder.isPaymentReceived; - this.isPayoutPublished = builder.isPayoutPublished; - this.isWithdrawn = builder.isWithdrawn; - this.contractAsJson = builder.contractAsJson; - this.contract = builder.contract; + public TradeInfo(TradeInfoV1Builder builder) { + this.offer = builder.getOffer(); + this.tradeId = builder.getTradeId(); + this.shortId = builder.getShortId(); + this.date = builder.getDate(); + this.role = builder.getRole(); + this.txFeeAsLong = builder.getTxFeeAsLong(); + this.takerFeeAsLong = builder.getTakerFeeAsLong(); + this.takerFeeTxId = builder.getTakerFeeTxId(); + this.makerDepositTxId = builder.getMakerDepositTxId(); + this.takerDepositTxId = builder.getTakerDepositTxId(); + this.payoutTxId = builder.getPayoutTxId(); + this.amountAsLong = builder.getAmountAsLong(); + this.price = builder.getPrice(); + this.volume = builder.getVolume(); + this.tradingPeerNodeAddress = builder.getTradingPeerNodeAddress(); + this.state = builder.getState(); + this.phase = builder.getPhase(); + this.periodState = builder.getPeriodState(); + this.isDepositPublished = builder.isDepositPublished(); + this.isDepositUnlocked = builder.isDepositUnlocked(); + this.isPaymentSent = builder.isPaymentSent(); + this.isPaymentReceived = builder.isPaymentReceived(); + this.isPayoutPublished = builder.isPayoutPublished(); + this.isCompleted = builder.isCompleted(); + this.contractAsJson = builder.getContractAsJson(); + this.contract = builder.getContract(); } public static TradeInfo toTradeInfo(Trade trade) { @@ -114,35 +134,34 @@ public class TradeInfo implements Payload { } else { contractInfo = ContractInfo.emptyContract.get(); } - - return new TradeInfoBuilder() - .withOffer(toOfferInfo(trade.getOffer())) + + return new TradeInfoV1Builder() .withTradeId(trade.getId()) .withShortId(trade.getShortId()) .withDate(trade.getDate().getTime()) .withRole(role == null ? "" : role) .withTxFeeAsLong(trade.getTxFeeAsLong()) .withTakerFeeAsLong(trade.getTakerFeeAsLong()) - .withTakerFeeAsLong(trade.getTakerFeeAsLong()) .withTakerFeeTxId(trade.getTakerFeeTxId()) .withMakerDepositTxId(trade.getMaker().getDepositTxHash()) .withTakerDepositTxId(trade.getTaker().getDepositTxHash()) .withPayoutTxId(trade.getPayoutTxId()) - .withTradeAmountAsLong(trade.getTradeAmountAsLong()) - .withTradePrice(trade.getTradePrice().getValue()) - .withTradingPeerNodeAddress(Objects.requireNonNull( - trade.getTradingPeerNodeAddress()).getHostNameWithoutPostFix()) + .withAmountAsLong(trade.getAmountAsLong()) + .withPrice(toPreciseTradePrice.apply(trade)) + .withVolume(toRoundedVolume.apply(trade)) + .withTradingPeerNodeAddress(toPeerNodeAddress.apply(trade)) .withState(trade.getState().name()) .withPhase(trade.getPhase().name()) - .withTradePeriodState(trade.getTradePeriodState().name()) + .withPeriodState(trade.getPeriodState().name()) .withIsDepositPublished(trade.isDepositPublished()) .withIsDepositUnlocked(trade.isDepositConfirmed()) .withIsPaymentSent(trade.isPaymentSent()) .withIsPaymentReceived(trade.isPaymentReceived()) .withIsPayoutPublished(trade.isPayoutPublished()) - .withIsWithdrawn(trade.isWithdrawn()) + .withIsCompleted(trade.isCompleted()) .withContractAsJson(trade.getContractAsJson()) .withContract(contractInfo) + .withOffer(toOfferInfo(trade.getOffer())) .build(); } @@ -164,25 +183,26 @@ public class TradeInfo implements Payload { .setMakerDepositTxId(makerDepositTxId == null ? "" : makerDepositTxId) .setTakerDepositTxId(takerDepositTxId == null ? "" : takerDepositTxId) .setPayoutTxId(payoutTxId == null ? "" : payoutTxId) - .setTradeAmountAsLong(tradeAmountAsLong) - .setTradePrice(tradePrice) + .setAmountAsLong(amountAsLong) + .setPrice(price) + .setTradeVolume(volume) .setTradingPeerNodeAddress(tradingPeerNodeAddress) .setState(state) .setPhase(phase) - .setTradePeriodState(tradePeriodState) + .setPeriodState(periodState) .setIsDepositPublished(isDepositPublished) .setIsDepositUnlocked(isDepositUnlocked) .setIsPaymentSent(isPaymentSent) .setIsPaymentReceived(isPaymentReceived) .setIsPayoutPublished(isPayoutPublished) - .setIsWithdrawn(isWithdrawn) + .setIsPayoutPublished(isCompleted) .setContractAsJson(contractAsJson == null ? "" : contractAsJson) .setContract(contract.toProtoMessage()) .build(); } public static TradeInfo fromProto(bisq.proto.grpc.TradeInfo proto) { - return new TradeInfoBuilder() + return new TradeInfoV1Builder() .withOffer(OfferInfo.fromProto(proto.getOffer())) .withTradeId(proto.getTradeId()) .withShortId(proto.getShortId()) @@ -194,9 +214,10 @@ public class TradeInfo implements Payload { .withMakerDepositTxId(proto.getMakerDepositTxId()) .withTakerDepositTxId(proto.getTakerDepositTxId()) .withPayoutTxId(proto.getPayoutTxId()) - .withTradeAmountAsLong(proto.getTradeAmountAsLong()) - .withTradePrice(proto.getTradePrice()) - .withTradePeriodState(proto.getTradePeriodState()) + .withAmountAsLong(proto.getAmountAsLong()) + .withPrice(proto.getPrice()) + .withVolume(proto.getTradeVolume()) + .withPeriodState(proto.getPeriodState()) .withState(proto.getState()) .withPhase(proto.getPhase()) .withTradingPeerNodeAddress(proto.getTradingPeerNodeAddress()) @@ -205,175 +226,12 @@ public class TradeInfo implements Payload { .withIsPaymentSent(proto.getIsPaymentSent()) .withIsPaymentReceived(proto.getIsPaymentReceived()) .withIsPayoutPublished(proto.getIsPayoutPublished()) - .withIsWithdrawn(proto.getIsWithdrawn()) + .withIsCompleted(proto.getIsCompleted()) .withContractAsJson(proto.getContractAsJson()) .withContract((ContractInfo.fromProto(proto.getContract()))) .build(); } - /* - * TradeInfoBuilder helps avoid bungling use of a large TradeInfo constructor - * argument list. If consecutive argument values of the same type are not - * ordered correctly, the compiler won't complain but the resulting bugs could - * be hard to find and fix. - */ - public static class TradeInfoBuilder { - private OfferInfo offer; - private String tradeId; - private String shortId; - private long date; - private String role; - private long txFeeAsLong; - private long takerFeeAsLong; - private String takerFeeTxId; - private String makerDepositTxId; - private String takerDepositTxId; - private String payoutTxId; - private long tradeAmountAsLong; - private long tradePrice; - private String tradingPeerNodeAddress; - private String state; - private String phase; - private String tradePeriodState; - private boolean isDepositPublished; - private boolean isDepositConfirmed; - private boolean isPaymentSent; - private boolean isPaymentReceived; - private boolean isPayoutPublished; - private boolean isWithdrawn; - private String contractAsJson; - private ContractInfo contract; - - public TradeInfoBuilder withOffer(OfferInfo offer) { - this.offer = offer; - return this; - } - - public TradeInfoBuilder withTradeId(String tradeId) { - this.tradeId = tradeId; - return this; - } - - public TradeInfoBuilder withShortId(String shortId) { - this.shortId = shortId; - return this; - } - - public TradeInfoBuilder withDate(long date) { - this.date = date; - return this; - } - - public TradeInfoBuilder withRole(String role) { - this.role = role; - return this; - } - - public TradeInfoBuilder withTxFeeAsLong(long txFeeAsLong) { - this.txFeeAsLong = txFeeAsLong; - return this; - } - - public TradeInfoBuilder withTakerFeeAsLong(long takerFeeAsLong) { - this.takerFeeAsLong = takerFeeAsLong; - return this; - } - - public TradeInfoBuilder withTakerFeeTxId(String takerFeeTxId) { - this.takerFeeTxId = takerFeeTxId; - return this; - } - - public TradeInfoBuilder withMakerDepositTxId(String makerDepositTxId) { - this.makerDepositTxId = makerDepositTxId; - return this; - } - - public TradeInfoBuilder withTakerDepositTxId(String takerDepositTxId) { - this.takerDepositTxId = takerDepositTxId; - return this; - } - - public TradeInfoBuilder withPayoutTxId(String payoutTxId) { - this.payoutTxId = payoutTxId; - return this; - } - - public TradeInfoBuilder withTradeAmountAsLong(long tradeAmountAsLong) { - this.tradeAmountAsLong = tradeAmountAsLong; - return this; - } - - public TradeInfoBuilder withTradePrice(long tradePrice) { - this.tradePrice = tradePrice; - return this; - } - - public TradeInfoBuilder withTradePeriodState(String tradePeriodState) { - this.tradePeriodState = tradePeriodState; - return this; - } - - public TradeInfoBuilder withState(String state) { - this.state = state; - return this; - } - - public TradeInfoBuilder withPhase(String phase) { - this.phase = phase; - return this; - } - - public TradeInfoBuilder withTradingPeerNodeAddress(String tradingPeerNodeAddress) { - this.tradingPeerNodeAddress = tradingPeerNodeAddress; - return this; - } - - public TradeInfoBuilder withIsDepositPublished(boolean isDepositPublished) { - this.isDepositPublished = isDepositPublished; - return this; - } - - public TradeInfoBuilder withIsDepositUnlocked(boolean isDepositConfirmed) { - this.isDepositConfirmed = isDepositConfirmed; - return this; - } - - public TradeInfoBuilder withIsPaymentSent(boolean isPaymentSent) { - this.isPaymentSent = isPaymentSent; - return this; - } - - public TradeInfoBuilder withIsPaymentReceived(boolean isPaymentReceived) { - this.isPaymentReceived = isPaymentReceived; - return this; - } - - public TradeInfoBuilder withIsPayoutPublished(boolean isPayoutPublished) { - this.isPayoutPublished = isPayoutPublished; - return this; - } - - public TradeInfoBuilder withIsWithdrawn(boolean isWithdrawn) { - this.isWithdrawn = isWithdrawn; - return this; - } - - public TradeInfoBuilder withContractAsJson(String contractAsJson) { - this.contractAsJson = contractAsJson; - return this; - } - - public TradeInfoBuilder withContract(ContractInfo contract) { - this.contract = contract; - return this; - } - - public TradeInfo build() { - return new TradeInfo(this); - } - } - @Override public String toString() { return "TradeInfo{" + @@ -387,18 +245,18 @@ public class TradeInfo implements Payload { ", makerDepositTxId='" + makerDepositTxId + '\'' + "\n" + ", takerDepositTxId='" + takerDepositTxId + '\'' + "\n" + ", payoutTxId='" + payoutTxId + '\'' + "\n" + - ", tradeAmountAsLong='" + tradeAmountAsLong + '\'' + "\n" + - ", tradePrice='" + tradePrice + '\'' + "\n" + + ", amountAsLong='" + amountAsLong + '\'' + "\n" + + ", price='" + price + '\'' + "\n" + ", tradingPeerNodeAddress='" + tradingPeerNodeAddress + '\'' + "\n" + ", state='" + state + '\'' + "\n" + ", phase='" + phase + '\'' + "\n" + - ", tradePeriodState='" + tradePeriodState + '\'' + "\n" + + ", periodState='" + periodState + '\'' + "\n" + ", isDepositPublished=" + isDepositPublished + "\n" + ", isDepositConfirmed=" + isDepositUnlocked + "\n" + ", isPaymentSent=" + isPaymentSent + "\n" + ", isPaymentReceived=" + isPaymentReceived + "\n" + ", isPayoutPublished=" + isPayoutPublished + "\n" + - ", isWithdrawn=" + isWithdrawn + "\n" + + ", isCompleted=" + isCompleted + "\n" + ", offer=" + offer + "\n" + ", contractAsJson=" + contractAsJson + "\n" + ", contract=" + contract + "\n" + diff --git a/core/src/main/java/bisq/core/api/model/builder/OfferInfoBuilder.java b/core/src/main/java/bisq/core/api/model/builder/OfferInfoBuilder.java new file mode 100644 index 0000000000..3948db907d --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/builder/OfferInfoBuilder.java @@ -0,0 +1,223 @@ +/* + * 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 . + */ + +package bisq.core.api.model.builder; + +import bisq.core.api.model.OfferInfo; + +import lombok.Getter; + +/* + * A builder helps avoid bungling use of a large OfferInfo constructor + * argument list. If consecutive argument values of the same type are not + * ordered correctly, the compiler won't complain but the resulting bugs could + * be hard to find and fix. + */ +@Getter +public final class OfferInfoBuilder { + + private String id; + private String direction; + private String price; + private boolean useMarketBasedPrice; + private double marketPriceMarginPct; + private long amount; + private long minAmount; + private String volume; + private String minVolume; + private long txFee; + private long makerFee; + private String offerFeePaymentTxId; + private long buyerSecurityDeposit; + private long sellerSecurityDeposit; + private String triggerPrice; + private boolean isCurrencyForMakerFeeBtc; + private String paymentAccountId; + private String paymentMethodId; + private String paymentMethodShortName; + private String baseCurrencyCode; + private String counterCurrencyCode; + private long date; + private String state; + private boolean isActivated; + private boolean isMyOffer; + private boolean isMyPendingOffer; + private boolean isBsqSwapOffer; + private String ownerNodeAddress; + private String pubKeyRing; + private String versionNumber; + private int protocolVersion; + + public OfferInfoBuilder withId(String id) { + this.id = id; + return this; + } + + public OfferInfoBuilder withDirection(String direction) { + this.direction = direction; + return this; + } + + public OfferInfoBuilder withPrice(String price) { + this.price = price; + return this; + } + + public OfferInfoBuilder withUseMarketBasedPrice(boolean useMarketBasedPrice) { + this.useMarketBasedPrice = useMarketBasedPrice; + return this; + } + + public OfferInfoBuilder withMarketPriceMarginPct(double marketPriceMarginPct) { + this.marketPriceMarginPct = marketPriceMarginPct; + return this; + } + + public OfferInfoBuilder withAmount(long amount) { + this.amount = amount; + return this; + } + + public OfferInfoBuilder withMinAmount(long minAmount) { + this.minAmount = minAmount; + return this; + } + + public OfferInfoBuilder withVolume(String volume) { + this.volume = volume; + return this; + } + + public OfferInfoBuilder withMinVolume(String minVolume) { + this.minVolume = minVolume; + return this; + } + + public OfferInfoBuilder withTxFee(long txFee) { + this.txFee = txFee; + return this; + } + + public OfferInfoBuilder withMakerFee(long makerFee) { + this.makerFee = makerFee; + return this; + } + + public OfferInfoBuilder withOfferFeePaymentTxId(String offerFeePaymentTxId) { + this.offerFeePaymentTxId = offerFeePaymentTxId; + return this; + } + + public OfferInfoBuilder withBuyerSecurityDeposit(long buyerSecurityDeposit) { + this.buyerSecurityDeposit = buyerSecurityDeposit; + return this; + } + + public OfferInfoBuilder withSellerSecurityDeposit(long sellerSecurityDeposit) { + this.sellerSecurityDeposit = sellerSecurityDeposit; + return this; + } + + public OfferInfoBuilder withTriggerPrice(String triggerPrice) { + this.triggerPrice = triggerPrice; + return this; + } + + public OfferInfoBuilder withIsCurrencyForMakerFeeBtc(boolean isCurrencyForMakerFeeBtc) { + this.isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc; + return this; + } + + public OfferInfoBuilder withPaymentAccountId(String paymentAccountId) { + this.paymentAccountId = paymentAccountId; + return this; + } + + public OfferInfoBuilder withPaymentMethodId(String paymentMethodId) { + this.paymentMethodId = paymentMethodId; + return this; + } + + public OfferInfoBuilder withPaymentMethodShortName(String paymentMethodShortName) { + this.paymentMethodShortName = paymentMethodShortName; + return this; + } + + public OfferInfoBuilder withBaseCurrencyCode(String baseCurrencyCode) { + this.baseCurrencyCode = baseCurrencyCode; + return this; + } + + public OfferInfoBuilder withCounterCurrencyCode(String counterCurrencyCode) { + this.counterCurrencyCode = counterCurrencyCode; + return this; + } + + public OfferInfoBuilder withDate(long date) { + this.date = date; + return this; + } + + public OfferInfoBuilder withState(String state) { + this.state = state; + return this; + } + + public OfferInfoBuilder withIsActivated(boolean isActivated) { + this.isActivated = isActivated; + return this; + } + + public OfferInfoBuilder withIsMyOffer(boolean isMyOffer) { + this.isMyOffer = isMyOffer; + return this; + } + + public OfferInfoBuilder withIsMyPendingOffer(boolean isMyPendingOffer) { + this.isMyPendingOffer = isMyPendingOffer; + return this; + } + + public OfferInfoBuilder withIsBsqSwapOffer(boolean isBsqSwapOffer) { + this.isBsqSwapOffer = isBsqSwapOffer; + return this; + } + + public OfferInfoBuilder withOwnerNodeAddress(String ownerNodeAddress) { + this.ownerNodeAddress = ownerNodeAddress; + return this; + } + + public OfferInfoBuilder withPubKeyRing(String pubKeyRing) { + this.pubKeyRing = pubKeyRing; + return this; + } + + public OfferInfoBuilder withVersionNumber(String versionNumber) { + this.versionNumber = versionNumber; + return this; + } + + public OfferInfoBuilder withProtocolVersion(int protocolVersion) { + this.protocolVersion = protocolVersion; + return this; + } + + public OfferInfo build() { + return new OfferInfo(this); + } +} diff --git a/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java b/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java new file mode 100644 index 0000000000..eefd0e296c --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java @@ -0,0 +1,207 @@ +/* + * 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 . + */ + +package bisq.core.api.model.builder; + +import bisq.core.api.model.ContractInfo; +import bisq.core.api.model.OfferInfo; +import bisq.core.api.model.TradeInfo; + +import lombok.Getter; + +/** + * A builder helps avoid bungling use of a large TradeInfo constructor + * argument list. If consecutive argument values of the same type are not + * ordered correctly, the compiler won't complain but the resulting bugs could + * be hard to find and fix. + */ +@Getter +public final class TradeInfoV1Builder { + + private OfferInfo offer; + private String tradeId; + private String shortId; + private long date; + private String role; + private boolean isCurrencyForTakerFeeBtc; + private long txFeeAsLong; + private long takerFeeAsLong; + private String takerFeeTxId; + private String makerDepositTxId; + private String takerDepositTxId; + private String payoutTxId; + private long amountAsLong; + private String price; + private String volume; + private String tradingPeerNodeAddress; + private String state; + private String phase; + private String periodState; + private boolean isDepositPublished; + private boolean isDepositUnlocked; + private boolean isPaymentSent; + private boolean isPaymentReceived; + private boolean isPayoutPublished; + private boolean isCompleted; + private String contractAsJson; + private ContractInfo contract; + private String closingStatus; + + public TradeInfoV1Builder withOffer(OfferInfo offer) { + this.offer = offer; + return this; + } + + public TradeInfoV1Builder withTradeId(String tradeId) { + this.tradeId = tradeId; + return this; + } + + public TradeInfoV1Builder withShortId(String shortId) { + this.shortId = shortId; + return this; + } + + public TradeInfoV1Builder withDate(long date) { + this.date = date; + return this; + } + + public TradeInfoV1Builder withRole(String role) { + this.role = role; + return this; + } + + public TradeInfoV1Builder withIsCurrencyForTakerFeeBtc(boolean isCurrencyForTakerFeeBtc) { + this.isCurrencyForTakerFeeBtc = isCurrencyForTakerFeeBtc; + return this; + } + + public TradeInfoV1Builder withTxFeeAsLong(long txFeeAsLong) { + this.txFeeAsLong = txFeeAsLong; + return this; + } + + public TradeInfoV1Builder withTakerFeeAsLong(long takerFeeAsLong) { + this.takerFeeAsLong = takerFeeAsLong; + return this; + } + + public TradeInfoV1Builder withTakerFeeTxId(String takerFeeTxId) { + this.takerFeeTxId = takerFeeTxId; + return this; + } + + public TradeInfoV1Builder withMakerDepositTxId(String makerDepositTxId) { + this.makerDepositTxId = makerDepositTxId; + return this; + } + + public TradeInfoV1Builder withTakerDepositTxId(String takerDepositTxId) { + this.takerDepositTxId = takerDepositTxId; + return this; + } + + public TradeInfoV1Builder withPayoutTxId(String payoutTxId) { + this.payoutTxId = payoutTxId; + return this; + } + + public TradeInfoV1Builder withAmountAsLong(long amountAsLong) { + this.amountAsLong = amountAsLong; + return this; + } + + public TradeInfoV1Builder withPrice(String price) { + this.price = price; + return this; + } + + public TradeInfoV1Builder withVolume(String volume) { + this.volume = volume; + return this; + } + + public TradeInfoV1Builder withPeriodState(String periodState) { + this.periodState = periodState; + return this; + } + + public TradeInfoV1Builder withState(String state) { + this.state = state; + return this; + } + + public TradeInfoV1Builder withPhase(String phase) { + this.phase = phase; + return this; + } + + public TradeInfoV1Builder withTradingPeerNodeAddress(String tradingPeerNodeAddress) { + this.tradingPeerNodeAddress = tradingPeerNodeAddress; + return this; + } + + public TradeInfoV1Builder withIsDepositPublished(boolean isDepositPublished) { + this.isDepositPublished = isDepositPublished; + return this; + } + + public TradeInfoV1Builder withIsDepositUnlocked(boolean isDepositUnlocked) { + this.isDepositUnlocked = isDepositUnlocked; + return this; + } + + public TradeInfoV1Builder withIsPaymentSent(boolean isPaymentSent) { + this.isPaymentSent = isPaymentSent; + return this; + } + + public TradeInfoV1Builder withIsPaymentReceived(boolean isPaymentReceived) { + this.isPaymentReceived = isPaymentReceived; + return this; + } + + public TradeInfoV1Builder withIsPayoutPublished(boolean isPayoutPublished) { + this.isPayoutPublished = isPayoutPublished; + return this; + } + + public TradeInfoV1Builder withIsCompleted(boolean isCompleted) { + this.isCompleted = isCompleted; + return this; + } + + public TradeInfoV1Builder withContractAsJson(String contractAsJson) { + this.contractAsJson = contractAsJson; + return this; + } + + public TradeInfoV1Builder withContract(ContractInfo contract) { + this.contract = contract; + return this; + } + + public TradeInfoV1Builder withClosingStatus(String closingStatus) { + this.closingStatus = closingStatus; + return this; + } + + public TradeInfo build() { + return new TradeInfo(this); + } +} diff --git a/core/src/main/java/bisq/core/app/DomainInitialisation.java b/core/src/main/java/bisq/core/app/DomainInitialisation.java index ad3f68c5ff..43168a3e68 100644 --- a/core/src/main/java/bisq/core/app/DomainInitialisation.java +++ b/core/src/main/java/bisq/core/app/DomainInitialisation.java @@ -43,8 +43,8 @@ import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.refund.RefundManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.traderchat.TraderChatManager; +import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.TradeManager; -import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.failed.FailedTradesManager; import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.trade.txproof.xmr.XmrTxProofService; diff --git a/core/src/main/java/bisq/core/btc/Balances.java b/core/src/main/java/bisq/core/btc/Balances.java index f067928628..e197c53047 100644 --- a/core/src/main/java/bisq/core/btc/Balances.java +++ b/core/src/main/java/bisq/core/btc/Balances.java @@ -25,9 +25,9 @@ import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.refund.RefundManager; +import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; -import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.failed.FailedTradesManager; import bisq.core.util.ParsingUtils; import bisq.network.p2p.P2PService; diff --git a/core/src/main/java/bisq/core/locale/CountryUtil.java b/core/src/main/java/bisq/core/locale/CountryUtil.java index 785ae70b5d..9ae9e7a321 100644 --- a/core/src/main/java/bisq/core/locale/CountryUtil.java +++ b/core/src/main/java/bisq/core/locale/CountryUtil.java @@ -21,6 +21,7 @@ import com.google.common.collect.Collections2; import com.google.common.collect.Lists; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -61,7 +62,7 @@ public class CountryUtil { String[] codes = {"AU", "CA", "FR", "DE", "IT", "NL", "ES", "GB", "IN", "JP", "SA", "SE", "SG", "TR", "US"}; populateCountryListByCodes(list, codes); - list.sort((a, b) -> a.name.compareTo(b.name)); + list.sort(Comparator.comparing(a -> a.name)); return list; } @@ -84,9 +85,8 @@ public class CountryUtil { } public static boolean containsAllSepaEuroCountries(List countryCodesToCompare) { - countryCodesToCompare.sort(String::compareTo); List countryCodesBase = getAllSepaEuroCountries().stream().map(c -> c.code).collect(Collectors.toList()); - return countryCodesToCompare.toString().equals(countryCodesBase.toString()); + return countryCodesToCompare.containsAll(countryCodesBase) && countryCodesBase.containsAll(countryCodesToCompare); } public static boolean containsAllSepaInstantEuroCountries(List countryCodesToCompare) { diff --git a/core/src/main/java/bisq/core/locale/CurrencyUtil.java b/core/src/main/java/bisq/core/locale/CurrencyUtil.java index 781f3713b3..903587653b 100644 --- a/core/src/main/java/bisq/core/locale/CurrencyUtil.java +++ b/core/src/main/java/bisq/core/locale/CurrencyUtil.java @@ -49,7 +49,6 @@ import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; -import static com.google.common.base.Preconditions.checkArgument; import static java.lang.String.format; @Slf4j @@ -70,17 +69,27 @@ public class CurrencyUtil { private static final Map isFiatCurrencyMap = new ConcurrentHashMap<>(); private static final Map isCryptoCurrencyMap = new ConcurrentHashMap<>(); - private static Supplier> fiatCurrencyMapSupplier = Suppliers.memoize( - CurrencyUtil::createFiatCurrencyMap)::get; - private static Supplier> cryptoCurrencyMapSupplier = Suppliers.memoize( - CurrencyUtil::createCryptoCurrencyMap)::get; + private static final Supplier> fiatCurrencyMapSupplier = Suppliers.memoize( + CurrencyUtil::createFiatCurrencyMap); + private static final Supplier> cryptoCurrencyMapSupplier = Suppliers.memoize( + CurrencyUtil::createCryptoCurrencyMap); public static void setBaseCurrencyCode(String baseCurrencyCode) { CurrencyUtil.baseCurrencyCode = baseCurrencyCode; } public static Collection getAllSortedFiatCurrencies() { - return fiatCurrencyMapSupplier.get().values(); + return fiatCurrencyMapSupplier.get().values(); // sorted by currency name + } + + public static List getAllFiatCurrencies() { + return new ArrayList<>(fiatCurrencyMapSupplier.get().values()); + } + + public static Collection getAllSortedFiatCurrencies(Comparator comparator) { + return (List) getAllSortedFiatCurrencies().stream() + .sorted(comparator) // sorted by comparator param + .collect(Collectors.toList()); } private static Map createFiatCurrencyMap() { @@ -160,217 +169,6 @@ public class CurrencyUtil { return currencies; } - public static List getAllAdvancedCashCurrencies() { - ArrayList currencies = new ArrayList<>(Arrays.asList( - new FiatCurrency("USD"), - new FiatCurrency("EUR"), - new FiatCurrency("GBP"), - new FiatCurrency("RUB"), - new FiatCurrency("UAH"), - new FiatCurrency("KZT"), - new FiatCurrency("BRL") - )); - currencies.sort(Comparator.comparing(TradeCurrency::getCode)); - return currencies; - } - - public static List getAllMoneyGramCurrencies() { - ArrayList currencies = new ArrayList<>(Arrays.asList( - new FiatCurrency("AED"), - new FiatCurrency("ARS"), - new FiatCurrency("AUD"), - new FiatCurrency("BND"), - new FiatCurrency("CAD"), - new FiatCurrency("CHF"), - new FiatCurrency("CZK"), - new FiatCurrency("DKK"), - new FiatCurrency("EUR"), - new FiatCurrency("FJD"), - new FiatCurrency("GBP"), - new FiatCurrency("HKD"), - new FiatCurrency("HUF"), - new FiatCurrency("IDR"), - new FiatCurrency("ILS"), - new FiatCurrency("INR"), - new FiatCurrency("JPY"), - new FiatCurrency("KRW"), - new FiatCurrency("KWD"), - new FiatCurrency("LKR"), - new FiatCurrency("MAD"), - new FiatCurrency("MGA"), - new FiatCurrency("MXN"), - new FiatCurrency("MYR"), - new FiatCurrency("NOK"), - new FiatCurrency("NZD"), - new FiatCurrency("OMR"), - new FiatCurrency("PEN"), - new FiatCurrency("PGK"), - new FiatCurrency("PHP"), - new FiatCurrency("PKR"), - new FiatCurrency("PLN"), - new FiatCurrency("SAR"), - new FiatCurrency("SBD"), - new FiatCurrency("SCR"), - new FiatCurrency("SEK"), - new FiatCurrency("SGD"), - new FiatCurrency("THB"), - new FiatCurrency("TOP"), - new FiatCurrency("TRY"), - new FiatCurrency("TWD"), - new FiatCurrency("USD"), - new FiatCurrency("VND"), - new FiatCurrency("VUV"), - new FiatCurrency("WST"), - new FiatCurrency("XOF"), - new FiatCurrency("XPF"), - new FiatCurrency("ZAR") - )); - - currencies.sort(Comparator.comparing(TradeCurrency::getCode)); - return currencies; - } - - // https://support.uphold.com/hc/en-us/articles/202473803-Supported-currencies - public static List getAllUpholdCurrencies() { - ArrayList currencies = new ArrayList<>(Arrays.asList( - new FiatCurrency("USD"), - new FiatCurrency("EUR"), - new FiatCurrency("GBP"), - new FiatCurrency("CNY"), - new FiatCurrency("JPY"), - new FiatCurrency("CHF"), - new FiatCurrency("INR"), - new FiatCurrency("MXN"), - new FiatCurrency("AUD"), - new FiatCurrency("CAD"), - new FiatCurrency("HKD"), - new FiatCurrency("NZD"), - new FiatCurrency("SGD"), - new FiatCurrency("KES"), - new FiatCurrency("ILS"), - new FiatCurrency("DKK"), - new FiatCurrency("NOK"), - new FiatCurrency("SEK"), - new FiatCurrency("PLN"), - new FiatCurrency("ARS"), - new FiatCurrency("BRL"), - new FiatCurrency("AED"), - new FiatCurrency("PHP") - )); - - currencies.sort(Comparator.comparing(TradeCurrency::getCode)); - return currencies; - } - - // https://github.com/bisq-network/proposals/issues/243 - public static List getAllTransferwiseCurrencies() { - ArrayList currencies = new ArrayList<>(Arrays.asList( - new FiatCurrency("ARS"), - new FiatCurrency("AUD"), - new FiatCurrency("XOF"), - new FiatCurrency("BGN"), - new FiatCurrency("CAD"), - new FiatCurrency("CLP"), - new FiatCurrency("HRK"), - new FiatCurrency("CZK"), - new FiatCurrency("DKK"), - new FiatCurrency("EGP"), - new FiatCurrency("EUR"), - new FiatCurrency("GEL"), - new FiatCurrency("HKD"), - new FiatCurrency("HUF"), - new FiatCurrency("IDR"), - new FiatCurrency("ILS"), - new FiatCurrency("JPY"), - new FiatCurrency("KES"), - new FiatCurrency("MYR"), - new FiatCurrency("MXN"), - new FiatCurrency("MAD"), - new FiatCurrency("NPR"), - new FiatCurrency("NZD"), - new FiatCurrency("NOK"), - new FiatCurrency("PKR"), - new FiatCurrency("PEN"), - new FiatCurrency("PHP"), - new FiatCurrency("PLN"), - new FiatCurrency("RON"), - new FiatCurrency("RUB"), - new FiatCurrency("SGD"), - new FiatCurrency("ZAR"), - new FiatCurrency("KRW"), - new FiatCurrency("SEK"), - new FiatCurrency("CHF"), - new FiatCurrency("THB"), - new FiatCurrency("TRY"), - new FiatCurrency("UGX"), - new FiatCurrency("AED"), - new FiatCurrency("GBP"), - new FiatCurrency("VND"), - new FiatCurrency("ZMW") - )); - - currencies.sort(Comparator.comparing(TradeCurrency::getCode)); - return currencies; - } - - public static List getAllAmazonGiftCardCurrencies() { - List currencies = new ArrayList<>(Arrays.asList( - new FiatCurrency("AUD"), - new FiatCurrency("CAD"), - new FiatCurrency("EUR"), - new FiatCurrency("GBP"), - new FiatCurrency("INR"), - new FiatCurrency("JPY"), - new FiatCurrency("SAR"), - new FiatCurrency("SEK"), - new FiatCurrency("SGD"), - new FiatCurrency("TRY"), - new FiatCurrency("USD") - )); - currencies.sort(Comparator.comparing(TradeCurrency::getCode)); - return currencies; - } - - // https://www.revolut.com/help/getting-started/exchanging-currencies/what-fiat-currencies-are-supported-for-holding-and-exchange - public static List getAllRevolutCurrencies() { - ArrayList currencies = new ArrayList<>(Arrays.asList( - new FiatCurrency("AED"), - new FiatCurrency("AUD"), - new FiatCurrency("BGN"), - new FiatCurrency("CAD"), - new FiatCurrency("CHF"), - new FiatCurrency("CZK"), - new FiatCurrency("DKK"), - new FiatCurrency("EUR"), - new FiatCurrency("GBP"), - new FiatCurrency("HKD"), - new FiatCurrency("HRK"), - new FiatCurrency("HUF"), - new FiatCurrency("ILS"), - new FiatCurrency("ISK"), - new FiatCurrency("JPY"), - new FiatCurrency("MAD"), - new FiatCurrency("MXN"), - new FiatCurrency("NOK"), - new FiatCurrency("NZD"), - new FiatCurrency("PLN"), - new FiatCurrency("QAR"), - new FiatCurrency("RON"), - new FiatCurrency("RSD"), - new FiatCurrency("RUB"), - new FiatCurrency("SAR"), - new FiatCurrency("SEK"), - new FiatCurrency("SGD"), - new FiatCurrency("THB"), - new FiatCurrency("TRY"), - new FiatCurrency("USD"), - new FiatCurrency("ZAR") - )); - - currencies.sort(Comparator.comparing(TradeCurrency::getCode)); - return currencies; - } - public static List getMatureMarketCurrencies() { ArrayList currencies = new ArrayList<>(Arrays.asList( new FiatCurrency("EUR"), @@ -401,9 +199,7 @@ public class CurrencyUtil { return isFiatCurrency; } catch (Throwable t) { - if (currencyCode != null) { - isFiatCurrencyMap.put(currencyCode, false); - } + isFiatCurrencyMap.put(currencyCode, false); return false; } } @@ -441,7 +237,7 @@ public class CurrencyUtil { // It might be that an asset was removed from the assetsRegistry, we deal with such cases below by checking if // it is a fiat currency isCryptoCurrency = true; - } else if (!getFiatCurrency(currencyCode).isPresent()) { + } else if (getFiatCurrency(currencyCode).isEmpty()) { // In case the code is from a removed asset we cross check if there exist a fiat currency with that code, // if we don't find a fiat currency we treat it as a crypto currency. isCryptoCurrency = true; @@ -517,7 +313,7 @@ public class CurrencyUtil { .findAny(); String xmrOrRemovedAsset = "XMR".equals(currencyCode) ? "Monero" : - removedCryptoCurrency.isPresent() ? removedCryptoCurrency.get().getName() : Res.get("shared.na"); + removedCryptoCurrency.isPresent() ? removedCryptoCurrency.get().getName() : Res.get("shared.na"); return getCryptoCurrency(currencyCode).map(TradeCurrency::getName).orElse(xmrOrRemovedAsset); } try { @@ -569,8 +365,6 @@ public class CurrencyUtil { return new CryptoCurrency(asset.getTickerSymbol(), asset.getName(), asset instanceof Token); } - - public static boolean assetMatchesCurrencyCode(Asset asset, String currencyCode) { return currencyCode.equals(asset.getTickerSymbol()); } @@ -581,10 +375,9 @@ public class CurrencyUtil { .filter(asset -> assetMatchesCurrencyCode(asset, currencyCode)).collect(Collectors.toList()); // If we don't have the ticker symbol we throw an exception - if (!assets.stream().findFirst().isPresent()) + if (assets.stream().findFirst().isEmpty()) return Optional.empty(); - // We check for exact match with network, e.g. BTC$TESTNET Optional optionalAssetMatchesNetwork = assets.stream() .filter(asset -> assetMatchesNetwork(asset, baseCurrencyNetwork)) @@ -596,8 +389,6 @@ public class CurrencyUtil { // that if no exact match was found in previous step if (!baseCurrencyNetwork.isMainnet()) { Optional optionalAsset = assets.stream().findFirst(); - checkArgument(optionalAsset.isPresent(), "optionalAsset must be present as we checked for " + - "not matching ticker symbols already above"); return optionalAsset; } @@ -653,4 +444,20 @@ public class CurrencyUtil { public static String getOfferVolumeCode(String currencyCode) { return Res.get("shared.offerVolumeCode", currencyCode); } + + public static boolean apiSupportsCryptoCurrency(String currencyCode) { + // Although this method is only used by the core.api package, its + // presence here avoids creating a new util class just for this method. + if (isCryptoCurrency(currencyCode)) + return currencyCode.equals("BTC") + || currencyCode.equals("XMR"); + else + throw new IllegalArgumentException( + format("Method requires a crypto currency code, but was given '%s'.", + currencyCode)); + } + + public static List getAllTransferwiseUSDCurrencies() { + return List.of(new FiatCurrency("USD")); + } } diff --git a/core/src/main/java/bisq/core/locale/GlobalSettings.java b/core/src/main/java/bisq/core/locale/GlobalSettings.java index 2d31bcf4df..75381832c3 100644 --- a/core/src/main/java/bisq/core/locale/GlobalSettings.java +++ b/core/src/main/java/bisq/core/locale/GlobalSettings.java @@ -23,6 +23,9 @@ import javafx.beans.property.SimpleObjectProperty; import java.util.Locale; +import lombok.extern.slf4j.Slf4j; + +@Slf4j public class GlobalSettings { private static boolean useAnimations = true; private static Locale locale; @@ -32,6 +35,7 @@ public class GlobalSettings { static { locale = Locale.getDefault(); + log.info("Locale info: {}", locale); // On some systems there is no country defined, in that case we use en_US if (locale.getCountry() == null || locale.getCountry().isEmpty()) diff --git a/core/src/main/java/bisq/core/locale/LanguageUtil.java b/core/src/main/java/bisq/core/locale/LanguageUtil.java index 2b6218b277..f675d522d9 100644 --- a/core/src/main/java/bisq/core/locale/LanguageUtil.java +++ b/core/src/main/java/bisq/core/locale/LanguageUtil.java @@ -43,7 +43,8 @@ public class LanguageUtil { "ja", // Japanese "fa", // Persian "it", // Italian - "cs" // Czech + "cs", // Czech + "pl" // Polish /* // not translated yet "el", // Greek @@ -54,7 +55,6 @@ public class LanguageUtil { "iw", // Hebrew "hi", // Hindi "ko", // Korean - "pl", // Polish "sv", // Swedish "no", // Norwegian "nl", // Dutch diff --git a/core/src/main/java/bisq/core/locale/Res.java b/core/src/main/java/bisq/core/locale/Res.java index aed0a48aad..bdc236db89 100644 --- a/core/src/main/java/bisq/core/locale/Res.java +++ b/core/src/main/java/bisq/core/locale/Res.java @@ -33,6 +33,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import java.util.MissingResourceException; import java.util.PropertyResourceBundle; @@ -42,6 +44,9 @@ import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; +import static bisq.common.util.Utilities.toListOfWrappedStrings; +import static java.nio.charset.StandardCharsets.UTF_8; + @Slf4j public class Res { public static void setup() { @@ -110,28 +115,45 @@ public class Res { public static String get(String key) { try { return resourceBundle.getString(key) - .replace("XMR", baseCurrencyCode) - .replace("Monero", baseCurrencyName) - .replace("monero", baseCurrencyNameLowerCase); + .replace("BTC", baseCurrencyCode) + .replace("Bitcoin", baseCurrencyName) + .replace("bitcoin", baseCurrencyNameLowerCase); } catch (MissingResourceException e) { log.warn("Missing resource for key: {}", key); - e.printStackTrace(); - if (DevEnv.isDevMode()) + if (DevEnv.isDevMode()) { + e.printStackTrace(); UserThread.runAfter(() -> { // We delay a bit to not throw while UI is not ready throw new RuntimeException("Missing resource for key: " + key); }, 1); - + } return key; } } + + public static List getWrappedAsList(String key, int wrapLength) { + String[] raw = get(key).split("\n"); + List wrapped = new ArrayList<>(); + for (String s : raw) { + List list = toListOfWrappedStrings(s, wrapLength); + for (String line : list) { + if (!line.isEmpty()) + wrapped.add(line); + } + } + return wrapped; + } } // Adds UTF8 support for property files class UTF8Control extends ResourceBundle.Control { - public ResourceBundle newBundle(String baseName, @NotNull Locale locale, @NotNull String format, ClassLoader loader, boolean reload) - throws IllegalAccessException, InstantiationException, IOException { + public ResourceBundle newBundle(String baseName, + @NotNull Locale locale, + @NotNull String format, + ClassLoader loader, + boolean reload) + throws IOException { // Below is a copy of the default implementation. final String bundleName = toBundleName(baseName, locale); final String resourceName = toResourceName(bundleName, "properties"); @@ -152,7 +174,7 @@ class UTF8Control extends ResourceBundle.Control { if (stream != null) { try { // Only this line is changed to make it read properties files as UTF-8. - bundle = new PropertyResourceBundle(new InputStreamReader(stream, "UTF-8")); + bundle = new PropertyResourceBundle(new InputStreamReader(stream, UTF_8)); } finally { stream.close(); } diff --git a/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java b/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java index ba8deacacf..e639059b0a 100644 --- a/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java +++ b/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java @@ -26,6 +26,7 @@ import bisq.core.notifications.MobileMessageType; import bisq.core.notifications.MobileNotificationService; import bisq.core.offer.Offer; import bisq.core.offer.OfferBookService; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferPayload; import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.PriceFeedService; @@ -109,7 +110,7 @@ public class MarketAlerts { // % price get multiplied by 10000 to have 0.12% be converted to 12. For fixed price we have precision of 8 for // altcoins and precision of 4 for fiat. private String getAlertId(Offer offer) { - double price = offer.isUseMarketBasedPrice() ? offer.getMarketPriceMargin() * 10000 : offer.getOfferPayload().getPrice(); + double price = offer.isUseMarketBasedPrice() ? offer.getMarketPriceMarginPct() * 10000 : offer.getOfferPayload().getPrice(); String priceString = String.valueOf((long) price); return offer.getId() + "|" + priceString; } @@ -119,7 +120,7 @@ public class MarketAlerts { MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); Price offerPrice = offer.getPrice(); if (marketPrice != null && offerPrice != null) { - boolean isSellOffer = offer.getDirection() == OfferPayload.Direction.SELL; + boolean isSellOffer = offer.getDirection() == OfferDirection.SELL; String shortOfferId = offer.getShortId(); boolean isFiatCurrency = CurrencyUtil.isFiatCurrency(currencyCode); String alertId = getAlertId(offer); diff --git a/core/src/main/java/bisq/core/offer/CreateOfferService.java b/core/src/main/java/bisq/core/offer/CreateOfferService.java index ccfb833138..2283cb3641 100644 --- a/core/src/main/java/bisq/core/offer/CreateOfferService.java +++ b/core/src/main/java/bisq/core/offer/CreateOfferService.java @@ -58,6 +58,8 @@ import java.util.UUID; import lombok.extern.slf4j.Slf4j; +import static bisq.core.payment.payload.PaymentMethod.HAL_CASH_ID; + @Slf4j @Singleton public class CreateOfferService { @@ -109,7 +111,7 @@ public class CreateOfferService { } public Offer createAndGetOffer(String offerId, - OfferPayload.Direction direction, + OfferDirection direction, String currencyCode, Coin amount, Coin minAmount, @@ -143,7 +145,7 @@ public class CreateOfferService { NodeAddress makerAddress = p2PService.getAddress(); boolean useMarketBasedPriceValue = useMarketBasedPrice && isMarketPriceAvailable(currencyCode) && - !paymentAccount.isHalCashAccount(); + !paymentAccount.hasPaymentMethodWithId(HAL_CASH_ID); long priceAsLong = price != null && !useMarketBasedPriceValue ? price.getValue() : 0L; double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0; @@ -198,7 +200,7 @@ public class CreateOfferService { creationTime, makerAddress, pubKeyRingProvider.get(), - OfferPayload.Direction.valueOf(direction.name()), + OfferDirection.valueOf(direction.name()), priceAsLong, marketPriceMarginParam, useMarketBasedPriceValue, @@ -238,7 +240,7 @@ public class CreateOfferService { } public Tuple2 getEstimatedFeeAndTxVsize(Coin amount, - OfferPayload.Direction direction, + OfferDirection direction, double buyerSecurityDeposit, double sellerSecurityDeposit) { Coin reservedFundsForOffer = getReservedFundsForOffer(direction, @@ -249,7 +251,7 @@ public class CreateOfferService { offerUtil.getMakerFee(amount)); } - public Coin getReservedFundsForOffer(OfferPayload.Direction direction, + public Coin getReservedFundsForOffer(OfferDirection direction, Coin amount, double buyerSecurityDeposit, double sellerSecurityDeposit) { @@ -264,7 +266,7 @@ public class CreateOfferService { return reservedFundsForOffer; } - public Coin getSecurityDeposit(OfferPayload.Direction direction, + public Coin getSecurityDeposit(OfferDirection direction, Coin amount, double buyerSecurityDeposit, double sellerSecurityDeposit) { diff --git a/core/src/main/java/bisq/core/offer/Offer.java b/core/src/main/java/bisq/core/offer/Offer.java index 79c899318a..319125268b 100644 --- a/core/src/main/java/bisq/core/offer/Offer.java +++ b/core/src/main/java/bisq/core/offer/Offer.java @@ -22,7 +22,6 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; -import bisq.core.offer.OfferPayload.Direction; import bisq.core.offer.availability.OfferAvailabilityModel; import bisq.core.offer.availability.OfferAvailabilityProtocol; import bisq.core.payment.payload.PaymentMethod; @@ -75,7 +74,6 @@ public class Offer implements NetworkPayload, PersistablePayload { // from one provider. private final static double PRICE_TOLERANCE = 0.01; - /////////////////////////////////////////////////////////////////////////////////////////// // Enums /////////////////////////////////////////////////////////////////////////////////////////// @@ -166,45 +164,56 @@ public class Offer implements NetworkPayload, PersistablePayload { @Nullable public Price getPrice() { String currencyCode = getCurrencyCode(); - if (offerPayload.isUseMarketBasedPrice()) { - checkNotNull(priceFeedService, "priceFeed must not be null"); - MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); - if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { - double factor; - double marketPriceMargin = offerPayload.getMarketPriceMargin(); - if (CurrencyUtil.isCryptoCurrency(currencyCode)) { - factor = getDirection() == OfferPayload.Direction.SELL ? - 1 - marketPriceMargin : 1 + marketPriceMargin; - } else { - factor = getDirection() == OfferPayload.Direction.BUY ? - 1 - marketPriceMargin : 1 + marketPriceMargin; - } - double marketPriceAsDouble = marketPrice.getPrice(); - double targetPriceAsDouble = marketPriceAsDouble * factor; - try { - int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? - Altcoin.SMALLEST_UNIT_EXPONENT : - Fiat.SMALLEST_UNIT_EXPONENT; - double scaled = MathUtils.scaleUpByPowerOf10(targetPriceAsDouble, precision); - final long roundedToLong = MathUtils.roundDoubleToLong(scaled); - return Price.valueOf(currencyCode, roundedToLong); - } catch (Exception e) { - log.error("Exception at getPrice / parseToFiat: " + e.toString() + "\n" + - "That case should never happen."); - return null; - } + if (!offerPayload.isUseMarketBasedPrice()) { + return Price.valueOf(currencyCode, offerPayload.getPrice()); + } + + checkNotNull(priceFeedService, "priceFeed must not be null"); + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { + double factor; + double marketPriceMargin = offerPayload.getMarketPriceMarginPct(); + if (CurrencyUtil.isCryptoCurrency(currencyCode)) { + factor = getDirection() == OfferDirection.SELL ? + 1 - marketPriceMargin : 1 + marketPriceMargin; } else { - log.trace("We don't have a market price. " + - "That case could only happen if you don't have a price feed."); + factor = getDirection() == OfferDirection.BUY ? + 1 - marketPriceMargin : 1 + marketPriceMargin; + } + double marketPriceAsDouble = marketPrice.getPrice(); + double targetPriceAsDouble = marketPriceAsDouble * factor; + try { + int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + double scaled = MathUtils.scaleUpByPowerOf10(targetPriceAsDouble, precision); + final long roundedToLong = MathUtils.roundDoubleToLong(scaled); + return Price.valueOf(currencyCode, roundedToLong); + } catch (Exception e) { + log.error("Exception at getPrice / parseToFiat: " + e + "\n" + + "That case should never happen."); return null; } } else { - return Price.valueOf(currencyCode, offerPayload.getPrice()); + log.trace("We don't have a market price. " + + "That case could only happen if you don't have a price feed."); + return null; } } - public void checkTradePriceTolerance(long takersTradePrice) throws TradePriceOutOfToleranceException, + public long getFixedPrice() { + return offerPayload.getPrice(); + } + + public void verifyTakersTradePrice(long takersTradePrice) throws TradePriceOutOfToleranceException, MarketPriceNotAvailableException, IllegalArgumentException { + if (!isUseMarketBasedPrice()) { + checkArgument(takersTradePrice == getFixedPrice(), + "Takers price does not match offer price. " + + "Takers price=" + takersTradePrice + "; offer price=" + getFixedPrice()); + return; + } + Price tradePrice = Price.valueOf(getCurrencyCode(), takersTradePrice); Price offerPrice = getPrice(); if (offerPrice == null) @@ -234,17 +243,16 @@ public class Offer implements NetworkPayload, PersistablePayload { @Nullable public Volume getVolumeByAmount(Coin amount) { Price price = getPrice(); - if (price != null && amount != null) { - Volume volumeByAmount = price.getVolumeByAmount(amount); - if (offerPayload.getPaymentMethodId().equals(PaymentMethod.HAL_CASH_ID)) - volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); - else if (CurrencyUtil.isFiatCurrency(offerPayload.getCurrencyCode())) - volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); - - return volumeByAmount; - } else { + if (price == null || amount == null) { return null; } + Volume volumeByAmount = price.getVolumeByAmount(amount); + if (offerPayload.getPaymentMethodId().equals(PaymentMethod.HAL_CASH_ID)) + volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); + else if (isFiatOffer()) + volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); + + return volumeByAmount; } public void resetState() { @@ -257,11 +265,6 @@ public class Offer implements NetworkPayload, PersistablePayload { /////////////////////////////////////////////////////////////////////////////////////////// public void setState(Offer.State state) { - try { - throw new RuntimeException("Setting offer state: " + state); - } catch (Exception e) { - e.printStackTrace(); - } stateProperty().set(state); } @@ -285,7 +288,7 @@ public class Offer implements NetworkPayload, PersistablePayload { // get the amount needed for the maker to reserve the offer public Coin getReserveAmount() { Coin reserveAmount = getAmount(); - reserveAmount = reserveAmount.add(getDirection() == Direction.BUY ? + reserveAmount = reserveAmount.add(getDirection() == OfferDirection.BUY ? getBuyerSecurityDeposit() : getSellerSecurityDeposit()); return reserveAmount; @@ -329,7 +332,7 @@ public class Offer implements NetworkPayload, PersistablePayload { } public PaymentMethod getPaymentMethod() { - return PaymentMethod.getPaymentMethodById(offerPayload.getPaymentMethodId()); + return PaymentMethod.getPaymentMethod(offerPayload.getPaymentMethodId()); } // utils @@ -348,18 +351,17 @@ public class Offer implements NetworkPayload, PersistablePayload { } public boolean isBuyOffer() { - return getDirection() == OfferPayload.Direction.BUY; + return getDirection() == OfferDirection.BUY; } - public OfferPayload.Direction getMirroredDirection() { - return getDirection() == OfferPayload.Direction.BUY ? OfferPayload.Direction.SELL : OfferPayload.Direction.BUY; + public OfferDirection getMirroredDirection() { + return getDirection() == OfferDirection.BUY ? OfferDirection.SELL : OfferDirection.BUY; } public boolean isMyOffer(KeyRing keyRing) { return getPubKeyRing().equals(keyRing.getPubKeyRing()); } - public Optional getAccountAgeWitnessHashAsHex() { Map extraDataMap = getExtraDataMap(); if (extraDataMap != null && extraDataMap.containsKey(OfferPayload.ACCOUNT_AGE_WITNESS_HASH)) @@ -410,7 +412,7 @@ public class Offer implements NetworkPayload, PersistablePayload { // Delegate Getter (boilerplate code generated via IntelliJ generate delegate feature) /////////////////////////////////////////////////////////////////////////////////////////// - public OfferPayload.Direction getDirection() { + public OfferDirection getDirection() { return offerPayload.getDirection(); } @@ -449,6 +451,18 @@ public class Offer implements NetworkPayload, PersistablePayload { return currencyCode; } + public String getCounterCurrencyCode() { + return offerPayload.getCounterCurrencyCode(); + } + + public String getBaseCurrencyCode() { + return offerPayload.getBaseCurrencyCode(); + } + + public String getPaymentMethodId() { + return offerPayload.getPaymentMethodId(); + } + public long getProtocolVersion() { return offerPayload.getProtocolVersion(); } @@ -457,8 +471,8 @@ public class Offer implements NetworkPayload, PersistablePayload { return offerPayload.isUseMarketBasedPrice(); } - public double getMarketPriceMargin() { - return offerPayload.getMarketPriceMargin(); + public double getMarketPriceMarginPct() { + return offerPayload.getMarketPriceMarginPct(); } public NodeAddress getMakerNodeAddress() { @@ -503,27 +517,6 @@ public class Offer implements NetworkPayload, PersistablePayload { return offerPayload.isUseAutoClose(); } - public long getBlockHeightAtOfferCreation() { - return offerPayload.getBlockHeightAtOfferCreation(); - } - - @Nullable - public String getHashOfChallenge() { - return offerPayload.getHashOfChallenge(); - } - - public boolean isPrivateOffer() { - return offerPayload.isPrivateOffer(); - } - - public long getUpperClosePrice() { - return offerPayload.getUpperClosePrice(); - } - - public long getLowerClosePrice() { - return offerPayload.getLowerClosePrice(); - } - public boolean isUseReOpenAfterAutoClose() { return offerPayload.isUseReOpenAfterAutoClose(); } @@ -543,6 +536,14 @@ public class Offer implements NetworkPayload, PersistablePayload { return getCurrencyCode().equals("XMR"); } + public boolean isFiatOffer() { + return CurrencyUtil.isFiatCurrency(currencyCode); + } + + public byte[] getOfferPayloadHash() { + return offerPayload.getHash(); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -550,7 +551,8 @@ public class Offer implements NetworkPayload, PersistablePayload { Offer offer = (Offer) o; - if (offerPayload != null ? !offerPayload.equals(offer.offerPayload) : offer.offerPayload != null) return false; + if (offerPayload != null ? !offerPayload.equals(offer.offerPayload) : offer.offerPayload != null) + return false; //noinspection SimplifiableIfStatement if (getState() != offer.getState()) return false; return !(getErrorMessage() != null ? !getErrorMessage().equals(offer.getErrorMessage()) : offer.getErrorMessage() != null); diff --git a/core/src/main/java/bisq/core/offer/OfferBookService.java b/core/src/main/java/bisq/core/offer/OfferBookService.java index 20e7f42483..82f12c7732 100644 --- a/core/src/main/java/bisq/core/offer/OfferBookService.java +++ b/core/src/main/java/bisq/core/offer/OfferBookService.java @@ -20,7 +20,7 @@ package bisq.core.offer; import bisq.core.filter.FilterManager; import bisq.core.locale.Res; import bisq.core.provider.price.PriceFeedService; - +import bisq.core.util.JsonUtil; import bisq.network.p2p.BootstrapListener; import bisq.network.p2p.P2PService; import bisq.network.p2p.storage.HashMapChangedListener; @@ -240,7 +240,7 @@ public class OfferBookService { offer.getDate(), offer.getId(), offer.isUseMarketBasedPrice(), - offer.getMarketPriceMargin(), + offer.getMarketPriceMarginPct(), offer.getPaymentMethod() ); } catch (Throwable t) { @@ -250,6 +250,6 @@ public class OfferBookService { }) .filter(Objects::nonNull) .collect(Collectors.toList()); - jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(offerForJsonList), "offers_statistics"); + jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(offerForJsonList), "offers_statistics"); } } diff --git a/core/src/main/java/bisq/core/offer/OfferDirection.java b/core/src/main/java/bisq/core/offer/OfferDirection.java new file mode 100644 index 0000000000..b284d10b0f --- /dev/null +++ b/core/src/main/java/bisq/core/offer/OfferDirection.java @@ -0,0 +1,33 @@ +/* + * 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 . + */ + +package bisq.core.offer; + +import bisq.common.proto.ProtoUtil; + +public enum OfferDirection { + BUY, + SELL; + + public static OfferDirection fromProto(protobuf.OfferDirection direction) { + return ProtoUtil.enumFromProto(OfferDirection.class, direction.name()); + } + + public static protobuf.OfferDirection toProtoMessage(OfferDirection direction) { + return protobuf.OfferDirection.valueOf(direction.name()); + } +} diff --git a/core/src/main/java/bisq/core/offer/OfferFilterService.java b/core/src/main/java/bisq/core/offer/OfferFilterService.java new file mode 100644 index 0000000000..b53b8bc2a3 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/OfferFilterService.java @@ -0,0 +1,210 @@ +/* + * 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 . + */ + +package bisq.core.offer; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.filter.FilterManager; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountUtil; +import bisq.core.user.Preferences; +import bisq.core.user.User; + +import bisq.common.app.DevEnv; +import bisq.common.app.Version; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import javafx.collections.SetChangeListener; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class OfferFilterService { + private final User user; + private final Preferences preferences; + private final FilterManager filterManager; + private final AccountAgeWitnessService accountAgeWitnessService; + private final Map insufficientCounterpartyTradeLimitCache = new HashMap<>(); + private final Map myInsufficientTradeLimitCache = new HashMap<>(); + + @Inject + public OfferFilterService(User user, + Preferences preferences, + FilterManager filterManager, + AccountAgeWitnessService accountAgeWitnessService) { + this.user = user; + this.preferences = preferences; + this.filterManager = filterManager; + this.accountAgeWitnessService = accountAgeWitnessService; + + if (user != null) { + // If our accounts have changed we reset our myInsufficientTradeLimitCache as it depends on account data + user.getPaymentAccountsAsObservable().addListener((SetChangeListener) c -> + myInsufficientTradeLimitCache.clear()); + } + } + + public enum Result { + VALID(true), + API_DISABLED, + HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER, + HAS_NOT_SAME_PROTOCOL_VERSION, + IS_IGNORED, + IS_OFFER_BANNED, + IS_CURRENCY_BANNED, + IS_PAYMENT_METHOD_BANNED, + IS_NODE_ADDRESS_BANNED, + REQUIRE_UPDATE_TO_NEW_VERSION, + IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT, + IS_MY_INSUFFICIENT_TRADE_LIMIT, + HIDE_BSQ_SWAPS_DUE_DAO_DEACTIVATED; + + @Getter + private final boolean isValid; + + Result(boolean isValid) { + this.isValid = isValid; + } + + Result() { + this(false); + } + } + + public Result canTakeOffer(Offer offer, boolean isTakerApiUser) { + if (isTakerApiUser && filterManager.getFilter() != null && filterManager.getFilter().isDisableApi()) { + return Result.API_DISABLED; + } + if (!isAnyPaymentAccountValidForOffer(offer)) { + return Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER; + } + if (!hasSameProtocolVersion(offer)) { + return Result.HAS_NOT_SAME_PROTOCOL_VERSION; + } + if (isIgnored(offer)) { + return Result.IS_IGNORED; + } + if (isOfferBanned(offer)) { + return Result.IS_OFFER_BANNED; + } + if (isCurrencyBanned(offer)) { + return Result.IS_CURRENCY_BANNED; + } + if (isPaymentMethodBanned(offer)) { + return Result.IS_PAYMENT_METHOD_BANNED; + } + if (isNodeAddressBanned(offer)) { + return Result.IS_NODE_ADDRESS_BANNED; + } + if (requireUpdateToNewVersion()) { + return Result.REQUIRE_UPDATE_TO_NEW_VERSION; + } + if (isInsufficientCounterpartyTradeLimit(offer)) { + return Result.IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT; + } + if (isMyInsufficientTradeLimit(offer)) { + return Result.IS_MY_INSUFFICIENT_TRADE_LIMIT; + } + + return Result.VALID; + } + + public boolean isAnyPaymentAccountValidForOffer(Offer offer) { + return user.getPaymentAccounts() != null && + PaymentAccountUtil.isAnyPaymentAccountValidForOffer(offer, user.getPaymentAccounts()); + } + + public boolean hasSameProtocolVersion(Offer offer) { + return offer.getProtocolVersion() == Version.TRADE_PROTOCOL_VERSION; + } + + public boolean isIgnored(Offer offer) { + return preferences.getIgnoreTradersList().stream() + .anyMatch(i -> i.equals(offer.getMakerNodeAddress().getFullAddress())); + } + + public boolean isOfferBanned(Offer offer) { + return filterManager.isOfferIdBanned(offer.getId()); + } + + public boolean isCurrencyBanned(Offer offer) { + return filterManager.isCurrencyBanned(offer.getCurrencyCode()); + } + + public boolean isPaymentMethodBanned(Offer offer) { + return filterManager.isPaymentMethodBanned(offer.getPaymentMethod()); + } + + public boolean isNodeAddressBanned(Offer offer) { + return filterManager.isNodeAddressBanned(offer.getMakerNodeAddress()); + } + + public boolean requireUpdateToNewVersion() { + return filterManager.requireUpdateToNewVersionForTrading(); + } + + // This call is a bit expensive so we cache results + public boolean isInsufficientCounterpartyTradeLimit(Offer offer) { + String offerId = offer.getId(); + if (insufficientCounterpartyTradeLimitCache.containsKey(offerId)) { + return insufficientCounterpartyTradeLimitCache.get(offerId); + } + + boolean result = offer.isFiatOffer() && + !accountAgeWitnessService.verifyPeersTradeAmount(offer, offer.getAmount(), + errorMessage -> { + }); + insufficientCounterpartyTradeLimitCache.put(offerId, result); + return result; + } + + // This call is a bit expensive so we cache results + public boolean isMyInsufficientTradeLimit(Offer offer) { + String offerId = offer.getId(); + if (myInsufficientTradeLimitCache.containsKey(offerId)) { + return myInsufficientTradeLimitCache.get(offerId); + } + + Optional accountOptional = PaymentAccountUtil.getMostMaturePaymentAccountForOffer(offer, + user.getPaymentAccounts(), + accountAgeWitnessService); + long myTradeLimit = accountOptional + .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, + offer.getCurrencyCode(), offer.getMirroredDirection())) + .orElse(0L); + long offerMinAmount = offer.getMinAmount().value; + log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ", + accountOptional.isPresent() ? accountOptional.get().getAccountName() : "null", + Coin.valueOf(myTradeLimit).toFriendlyString(), + Coin.valueOf(offerMinAmount).toFriendlyString()); + boolean result = offer.isFiatOffer() && + accountOptional.isPresent() && + myTradeLimit < offerMinAmount; + myInsufficientTradeLimitCache.put(offerId, result); + return result; + } +} diff --git a/core/src/main/java/bisq/core/offer/OfferForJson.java b/core/src/main/java/bisq/core/offer/OfferForJson.java index f4acd95b01..093d5be87b 100644 --- a/core/src/main/java/bisq/core/offer/OfferForJson.java +++ b/core/src/main/java/bisq/core/offer/OfferForJson.java @@ -40,7 +40,7 @@ import javax.annotation.Nullable; public class OfferForJson { private static final Logger log = LoggerFactory.getLogger(OfferForJson.class); - public final OfferPayload.Direction direction; + public final OfferDirection direction; public final String currencyCode; public final long minAmount; public final long amount; @@ -53,7 +53,7 @@ public class OfferForJson { // primaryMarket fields are based on industry standard where primaryMarket is always in the focus (in the app BTC is always in the focus - will be changed in a larger refactoring once) public String currencyPair; - public OfferPayload.Direction primaryMarketDirection; + public OfferDirection primaryMarketDirection; public String priceDisplayString; public String primaryMarketAmountDisplayString; @@ -75,7 +75,7 @@ public class OfferForJson { transient private final MonetaryFormat coinFormat = MonetaryFormat.BTC; - public OfferForJson(OfferPayload.Direction direction, + public OfferForJson(OfferDirection direction, String currencyCode, Coin minAmount, Coin amount, @@ -104,7 +104,7 @@ public class OfferForJson { try { final Price price = getPrice(); if (CurrencyUtil.isCryptoCurrency(currencyCode)) { - primaryMarketDirection = direction == OfferPayload.Direction.BUY ? OfferPayload.Direction.SELL : OfferPayload.Direction.BUY; + primaryMarketDirection = direction == OfferDirection.BUY ? OfferDirection.SELL : OfferDirection.BUY; currencyPair = currencyCode + "/" + Res.getBaseCurrencyCode(); // int precision = 8; diff --git a/core/src/main/java/bisq/core/offer/OfferPayload.java b/core/src/main/java/bisq/core/offer/OfferPayload.java index f75b70a343..6e1ce18e39 100644 --- a/core/src/main/java/bisq/core/offer/OfferPayload.java +++ b/core/src/main/java/bisq/core/offer/OfferPayload.java @@ -21,22 +21,23 @@ import bisq.network.p2p.NodeAddress; import bisq.network.p2p.storage.payload.ExpirablePayload; import bisq.network.p2p.storage.payload.ProtectedStoragePayload; import bisq.network.p2p.storage.payload.RequiresOwnerIsOnlinePayload; - +import bisq.common.crypto.Hash; import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtoUtil; import bisq.common.util.CollectionUtils; -import bisq.common.util.ExtraDataMapValidator; +import bisq.common.util.Hex; import bisq.common.util.JsonExclude; - -import java.security.PublicKey; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; - +import java.lang.reflect.Type; +import java.security.PublicKey; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -44,35 +45,47 @@ import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - // OfferPayload has about 1.4 kb. We should look into options to make it smaller but will be hard to do it in a // backward compatible way. Maybe a candidate when segwit activation is done as hardfork? - -@EqualsAndHashCode +@EqualsAndHashCode(exclude = {"hash"}) @Getter @Slf4j public final class OfferPayload implements ProtectedStoragePayload, ExpirablePayload, RequiresOwnerIsOnlinePayload { public static final long TTL = TimeUnit.MINUTES.toMillis(9); - /////////////////////////////////////////////////////////////////////////////////////////// - // Enum - /////////////////////////////////////////////////////////////////////////////////////////// - - public enum Direction { - BUY, - SELL; - - public static OfferPayload.Direction fromProto(protobuf.OfferPayload.Direction direction) { - return ProtoUtil.enumFromProto(OfferPayload.Direction.class, direction.name()); - } - - public static protobuf.OfferPayload.Direction toProtoMessage(Direction direction) { - return protobuf.OfferPayload.Direction.valueOf(direction.name()); - } - } - + protected final String id; + protected final long date; + // For fiat offer the baseCurrencyCode is BTC and the counterCurrencyCode is the fiat currency + // For altcoin offers it is the opposite. baseCurrencyCode is the altcoin and the counterCurrencyCode is BTC. + protected final String baseCurrencyCode; + protected final String counterCurrencyCode; + // price if fixed price is used (usePercentageBasedPrice = false), otherwise 0 + protected final long price; + protected final long amount; + protected final long minAmount; + protected final String paymentMethodId; + protected final String makerPaymentAccountId; + protected final NodeAddress ownerNodeAddress; + protected final OfferDirection direction; + protected final String versionNr; + protected final int protocolVersion; + @JsonExclude + protected final PubKeyRing pubKeyRing; + // cache + protected transient byte[] hash; + @Nullable + protected final Map extraDataMap; + + // address and signature of signing arbitrator + @Setter + protected NodeAddress arbitratorSigner; + @Setter + @Nullable + protected String arbitratorSignature; + @Setter + @Nullable + protected List reserveTxKeyImages; + // Keys for extra map // Only set for fiat offers public static final String ACCOUNT_AGE_WITNESS_HASH = "accountAgeWitnessHash"; @@ -95,35 +108,18 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay // Instance fields /////////////////////////////////////////////////////////////////////////////////////////// - private final String id; - private final long date; - private final NodeAddress ownerNodeAddress; - @JsonExclude - private final PubKeyRing pubKeyRing; - private final Direction direction; - // price if fixed price is used (usePercentageBasedPrice = false), otherwise 0 - private final long price; // Distance form market price if percentage based price is used (usePercentageBasedPrice = true), otherwise 0. // E.g. 0.1 -> 10%. Can be negative as well. Depending on direction the marketPriceMargin is above or below the market price. // Positive values is always the usual case where you want a better price as the market. // E.g. Buy offer with market price 400.- leads to a 360.- price. // Sell offer with market price 400.- leads to a 440.- price. - private final double marketPriceMargin; + private final double marketPriceMarginPct; // We use 2 type of prices: fixed price or price based on distance from market price private final boolean useMarketBasedPrice; - private final long amount; - private final long minAmount; - // For fiat offer the baseCurrencyCode is BTC and the counterCurrencyCode is the fiat currency - // For altcoin offers it is the opposite. baseCurrencyCode is the altcoin and the counterCurrencyCode is BTC. - private final String baseCurrencyCode; - private final String counterCurrencyCode; - - private final String paymentMethodId; - private final String makerPaymentAccountId; - // Mutable property. Has to be set before offer is save in P2P network as it changes the objects hash! - @Nullable + // Mutable property. Has to be set before offer is saved in P2P network as it changes the payload hash! @Setter + @Nullable private String offerFeePaymentTxId; @Nullable private final String countryCode; @@ -133,7 +129,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay private final String bankId; @Nullable private final List acceptedBankIds; - private final String versionNr; private final long blockHeightAtOfferCreation; private final long txFee; private final long makerFee; @@ -157,26 +152,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay @Nullable private final String hashOfChallenge; - // Should be only used in emergency case if we need to add data but do not want to break backward compatibility - // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new - // field in a class would break that hash and therefore break the storage mechanism. - - // extraDataMap used from v0.6 on for hashOfPaymentAccount - // key ACCOUNT_AGE_WITNESS, value: hex string of hashOfPaymentAccount byte array - @Nullable - private final Map extraDataMap; - private final int protocolVersion; - - // address and signature of signing arbitrator - @Setter - private NodeAddress arbitratorSigner; - @Setter - @Nullable - private String arbitratorSignature; - @Setter - @Nullable - private List reserveTxKeyImages; - /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -186,9 +161,9 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay long date, NodeAddress ownerNodeAddress, PubKeyRing pubKeyRing, - Direction direction, + OfferDirection direction, long price, - double marketPriceMargin, + double marketPriceMarginPct, boolean useMarketBasedPrice, long amount, long minAmount, @@ -224,22 +199,27 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay this.date = date; this.ownerNodeAddress = ownerNodeAddress; this.pubKeyRing = pubKeyRing; - this.direction = direction; - this.price = price; - this.marketPriceMargin = marketPriceMargin; - this.useMarketBasedPrice = useMarketBasedPrice; - this.amount = amount; - this.minAmount = minAmount; this.baseCurrencyCode = baseCurrencyCode; this.counterCurrencyCode = counterCurrencyCode; + this.direction = direction; + this.price = price; + this.amount = amount; + this.minAmount = minAmount; this.paymentMethodId = paymentMethodId; this.makerPaymentAccountId = makerPaymentAccountId; + this.extraDataMap = extraDataMap; + this.versionNr = versionNr; + this.protocolVersion = protocolVersion; + this.arbitratorSigner = arbitratorSigner; + this.arbitratorSignature = arbitratorSignature; + this.reserveTxKeyImages = reserveTxKeyImages; + this.marketPriceMarginPct = marketPriceMarginPct; + this.useMarketBasedPrice = useMarketBasedPrice; this.offerFeePaymentTxId = offerFeePaymentTxId; this.countryCode = countryCode; this.acceptedCountryCodes = acceptedCountryCodes; this.bankId = bankId; this.acceptedBankIds = acceptedBankIds; - this.versionNr = versionNr; this.blockHeightAtOfferCreation = blockHeightAtOfferCreation; this.txFee = txFee; this.makerFee = makerFee; @@ -253,11 +233,34 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay this.upperClosePrice = upperClosePrice; this.isPrivateOffer = isPrivateOffer; this.hashOfChallenge = hashOfChallenge; - this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); - this.protocolVersion = protocolVersion; - this.arbitratorSigner = arbitratorSigner; - this.arbitratorSignature = arbitratorSignature; - this.reserveTxKeyImages = reserveTxKeyImages; + } + + public byte[] getHash() { + if (this.hash == null && this.offerFeePaymentTxId != null) { + // A proto message can be created only after the offerFeePaymentTxId is + // set to a non-null value; now is the time to cache the payload hash. + this.hash = Hash.getSha256Hash(this.toProtoMessage().toByteArray()); + } + return this.hash; + } + + @Override + public long getTTL() { + return TTL; + } + + @Override + public PublicKey getOwnerPubKey() { + return pubKeyRing.getSignaturePubKey(); + } + + // In the offer we support base and counter currency + // Fiat offers have base currency XMR and counterCurrency Fiat + // Altcoins have base currency Altcoin and counterCurrency XMR + // The rest of the app does not support yet that concept of base currency and counter currencies + // so we map here for convenience + public String getCurrencyCode() { + return getBaseCurrencyCode().equals("XMR") ? getCounterCurrencyCode() : getBaseCurrencyCode(); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -271,9 +274,9 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay .setDate(date) .setOwnerNodeAddress(ownerNodeAddress.toProtoMessage()) .setPubKeyRing(pubKeyRing.toProtoMessage()) - .setDirection(Direction.toProtoMessage(direction)) + .setDirection(OfferDirection.toProtoMessage(direction)) .setPrice(price) - .setMarketPriceMargin(marketPriceMargin) + .setMarketPriceMarginPct(marketPriceMarginPct) .setUseMarketBasedPrice(useMarketBasedPrice) .setAmount(amount) .setMinAmount(minAmount) @@ -296,7 +299,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay .setIsPrivateOffer(isPrivateOffer) .setProtocolVersion(protocolVersion) .setArbitratorSigner(arbitratorSigner.toProtoMessage()); - Optional.ofNullable(offerFeePaymentTxId).ifPresent(builder::setOfferFeePaymentTxId); Optional.ofNullable(countryCode).ifPresent(builder::setCountryCode); Optional.ofNullable(bankId).ifPresent(builder::setBankId); @@ -323,9 +325,9 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay proto.getDate(), NodeAddress.fromProto(proto.getOwnerNodeAddress()), PubKeyRing.fromProto(proto.getPubKeyRing()), - OfferPayload.Direction.fromProto(proto.getDirection()), + OfferDirection.fromProto(proto.getDirection()), proto.getPrice(), - proto.getMarketPriceMargin(), + proto.getMarketPriceMarginPct(), proto.getUseMarketBasedPrice(), proto.getAmount(), proto.getMinAmount(), @@ -333,7 +335,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay proto.getCounterCurrencyCode(), proto.getPaymentMethodId(), proto.getMakerPaymentAccountId(), - ProtoUtil.stringOrNullFromProto(proto.getOfferFeePaymentTxId()), + proto.getOfferFeePaymentTxId(), ProtoUtil.stringOrNullFromProto(proto.getCountryCode()), acceptedCountryCodes, ProtoUtil.stringOrNullFromProto(proto.getBankId()), @@ -359,70 +361,92 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay proto.getReserveTxKeyImagesList() == null ? null : new ArrayList(proto.getReserveTxKeyImagesList())); } - - /////////////////////////////////////////////////////////////////////////////////////////// - // API - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public long getTTL() { - return TTL; - } - - @Override - public PublicKey getOwnerPubKey() { - return pubKeyRing.getSignaturePubKey(); - } - - // In the offer we support base and counter currency - // Fiat offers have base currency XMR and counterCurrency Fiat - // Altcoins have base currency Altcoin and counterCurrency XMR - // The rest of the app does not support yet that concept of base currency and counter currencies - // so we map here for convenience - public String getCurrencyCode() { - return getBaseCurrencyCode().equals("XMR") ? getCounterCurrencyCode() : getBaseCurrencyCode(); - } - @Override public String toString() { return "OfferPayload{" + - "\n id='" + id + '\'' + - ",\n date=" + new Date(date) + - ",\n ownerNodeAddress=" + ownerNodeAddress + - ",\n pubKeyRing=" + pubKeyRing + - ",\n direction=" + direction + - ",\n price=" + price + - ",\n marketPriceMargin=" + marketPriceMargin + - ",\n useMarketBasedPrice=" + useMarketBasedPrice + - ",\n amount=" + amount + - ",\n minAmount=" + minAmount + - ",\n baseCurrencyCode='" + baseCurrencyCode + '\'' + - ",\n counterCurrencyCode='" + counterCurrencyCode + '\'' + - ",\n paymentMethodId='" + paymentMethodId + '\'' + - ",\n makerPaymentAccountId='" + makerPaymentAccountId + '\'' + - ",\n offerFeePaymentTxId='" + offerFeePaymentTxId + '\'' + - ",\n countryCode='" + countryCode + '\'' + - ",\n acceptedCountryCodes=" + acceptedCountryCodes + - ",\n bankId='" + bankId + '\'' + - ",\n acceptedBankIds=" + acceptedBankIds + - ",\n versionNr='" + versionNr + '\'' + - ",\n blockHeightAtOfferCreation=" + blockHeightAtOfferCreation + - ",\n txFee=" + txFee + - ",\n makerFee=" + makerFee + - ",\n buyerSecurityDeposit=" + buyerSecurityDeposit + - ",\n sellerSecurityDeposit=" + sellerSecurityDeposit + - ",\n maxTradeLimit=" + maxTradeLimit + - ",\n maxTradePeriod=" + maxTradePeriod + - ",\n useAutoClose=" + useAutoClose + - ",\n useReOpenAfterAutoClose=" + useReOpenAfterAutoClose + - ",\n lowerClosePrice=" + lowerClosePrice + - ",\n upperClosePrice=" + upperClosePrice + - ",\n isPrivateOffer=" + isPrivateOffer + - ",\n hashOfChallenge='" + hashOfChallenge + '\'' + - ",\n extraDataMap=" + extraDataMap + - ",\n protocolVersion=" + protocolVersion + + "\r\n id='" + id + '\'' + + ",\r\n date=" + date + + ",\r\n baseCurrencyCode='" + baseCurrencyCode + '\'' + + ",\r\n counterCurrencyCode='" + counterCurrencyCode + '\'' + + ",\r\n price=" + price + + ",\r\n amount=" + amount + + ",\r\n minAmount=" + minAmount + + ",\r\n paymentMethodId='" + paymentMethodId + '\'' + + ",\r\n makerPaymentAccountId='" + makerPaymentAccountId + '\'' + + ",\r\n ownerNodeAddress=" + ownerNodeAddress + + ",\r\n direction=" + direction + + ",\r\n versionNr='" + versionNr + '\'' + + ",\r\n protocolVersion=" + protocolVersion + + ",\r\n pubKeyRing=" + pubKeyRing + + ",\r\n hash=" + (hash != null ? Hex.encode(hash) : "null") + + ",\r\n extraDataMap=" + extraDataMap + + ",\r\n arbitratorSigner=" + arbitratorSigner + + ",\r\n arbitratorSignature=" + arbitratorSignature + + ",\r\n reserveTxKeyImages=" + reserveTxKeyImages + + ",\r\n marketPriceMargin=" + marketPriceMarginPct + + ",\r\n useMarketBasedPrice=" + useMarketBasedPrice + + ",\r\n offerFeePaymentTxId='" + offerFeePaymentTxId + '\'' + + ",\r\n countryCode='" + countryCode + '\'' + + ",\r\n acceptedCountryCodes=" + acceptedCountryCodes + + ",\r\n bankId='" + bankId + '\'' + + ",\r\n acceptedBankIds=" + acceptedBankIds + + ",\r\n blockHeightAtOfferCreation=" + blockHeightAtOfferCreation + + ",\r\n txFee=" + txFee + + ",\r\n makerFee=" + makerFee + + ",\r\n buyerSecurityDeposit=" + buyerSecurityDeposit + + ",\r\n sellerSecurityDeposit=" + sellerSecurityDeposit + + ",\r\n maxTradeLimit=" + maxTradeLimit + + ",\r\n maxTradePeriod=" + maxTradePeriod + + ",\r\n useAutoClose=" + useAutoClose + + ",\r\n useReOpenAfterAutoClose=" + useReOpenAfterAutoClose + + ",\r\n lowerClosePrice=" + lowerClosePrice + + ",\r\n upperClosePrice=" + upperClosePrice + + ",\r\n isPrivateOffer=" + isPrivateOffer + + ",\r\n hashOfChallenge='" + hashOfChallenge + '\'' + ",\n arbitratorSigner=" + arbitratorSigner + ",\n arbitratorSignature=" + arbitratorSignature + - "\n}"; + "\r\n} "; + } + + // For backward compatibility we need to ensure same order for json fields as with 1.7.5. and earlier versions. + // The json is used for the hash in the contract and change of oder would cause a different hash and + // therefore a failure during trade. + public static class JsonSerializer implements com.google.gson.JsonSerializer { + @Override + public JsonElement serialize(OfferPayload offerPayload, Type type, JsonSerializationContext context) { + JsonObject object = new JsonObject(); + object.add("id", context.serialize(offerPayload.getId())); + object.add("date", context.serialize(offerPayload.getDate())); + object.add("ownerNodeAddress", context.serialize(offerPayload.getOwnerNodeAddress())); + object.add("direction", context.serialize(offerPayload.getDirection())); + object.add("price", context.serialize(offerPayload.getPrice())); + object.add("marketPriceMargin", context.serialize(offerPayload.getMarketPriceMarginPct())); + object.add("useMarketBasedPrice", context.serialize(offerPayload.isUseMarketBasedPrice())); + object.add("amount", context.serialize(offerPayload.getAmount())); + object.add("minAmount", context.serialize(offerPayload.getMinAmount())); + object.add("baseCurrencyCode", context.serialize(offerPayload.getBaseCurrencyCode())); + object.add("counterCurrencyCode", context.serialize(offerPayload.getCounterCurrencyCode())); + object.add("paymentMethodId", context.serialize(offerPayload.getPaymentMethodId())); + object.add("makerPaymentAccountId", context.serialize(offerPayload.getMakerPaymentAccountId())); + object.add("offerFeePaymentTxId", context.serialize(offerPayload.getOfferFeePaymentTxId())); + object.add("versionNr", context.serialize(offerPayload.getVersionNr())); + object.add("blockHeightAtOfferCreation", context.serialize(offerPayload.getBlockHeightAtOfferCreation())); + object.add("txFee", context.serialize(offerPayload.getTxFee())); + object.add("makerFee", context.serialize(offerPayload.getMakerFee())); + object.add("buyerSecurityDeposit", context.serialize(offerPayload.getBuyerSecurityDeposit())); + object.add("sellerSecurityDeposit", context.serialize(offerPayload.getSellerSecurityDeposit())); + object.add("maxTradeLimit", context.serialize(offerPayload.getMaxTradeLimit())); + object.add("maxTradePeriod", context.serialize(offerPayload.getMaxTradePeriod())); + object.add("useAutoClose", context.serialize(offerPayload.isUseAutoClose())); + object.add("useReOpenAfterAutoClose", context.serialize(offerPayload.isUseReOpenAfterAutoClose())); + object.add("lowerClosePrice", context.serialize(offerPayload.getLowerClosePrice())); + object.add("upperClosePrice", context.serialize(offerPayload.getUpperClosePrice())); + object.add("isPrivateOffer", context.serialize(offerPayload.isPrivateOffer())); + object.add("extraDataMap", context.serialize(offerPayload.getExtraDataMap())); + object.add("protocolVersion", context.serialize(offerPayload.getProtocolVersion())); + object.add("arbitratorSigner", context.serialize(offerPayload.getArbitratorSigner())); + object.add("arbitratorSignature", context.serialize(offerPayload.getArbitratorSignature())); + return object; + } } } diff --git a/core/src/main/java/bisq/core/offer/OfferRestrictions.java b/core/src/main/java/bisq/core/offer/OfferRestrictions.java index a84d506d21..108b80587a 100644 --- a/core/src/main/java/bisq/core/offer/OfferRestrictions.java +++ b/core/src/main/java/bisq/core/offer/OfferRestrictions.java @@ -19,6 +19,7 @@ package bisq.core.offer; import bisq.common.app.Capabilities; import bisq.common.app.Capability; +import bisq.common.config.Config; import bisq.common.util.Utilities; import org.bitcoinj.core.Coin; @@ -28,18 +29,18 @@ import java.util.GregorianCalendar; import java.util.Map; public class OfferRestrictions { - // The date when traders who have not updated cannot take offers from updated clients and their offers become - // invisible for updated clients. - private static final Date REQUIRE_UPDATE_DATE = Utilities.getUTCDate(2019, GregorianCalendar.SEPTEMBER, 19); + // The date when traders who have not upgraded to a Tor v3 Node Address cannot take offers and their offers become + // invisible. + private static final Date REQUIRE_TOR_NODE_ADDRESS_V3_DATE = Utilities.getUTCDate(2021, GregorianCalendar.AUGUST, 15); - static boolean requiresUpdate() { - return new Date().after(REQUIRE_UPDATE_DATE); + public static boolean requiresNodeAddressUpdate() { + return new Date().after(REQUIRE_TOR_NODE_ADDRESS_V3_DATE) && !Config.baseCurrencyNetwork().isStagenet(); } public static Coin TOLERATED_SMALL_TRADE_AMOUNT = Coin.parseCoin("1.0"); static boolean hasOfferMandatoryCapability(Offer offer, Capability mandatoryCapability) { - Map extraDataMap = offer.getOfferPayload().getExtraDataMap(); + Map extraDataMap = offer.getExtraDataMap(); if (extraDataMap != null && extraDataMap.containsKey(OfferPayload.CAPABILITIES)) { String commaSeparatedOrdinals = extraDataMap.get(OfferPayload.CAPABILITIES); Capabilities capabilities = Capabilities.fromStringList(commaSeparatedOrdinals); diff --git a/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index 874b3378f7..e93861eb0f 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -33,13 +33,16 @@ import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.statistics.ReferralIdService; import bisq.core.user.AutoConfirmSettings; import bisq.core.user.Preferences; +import bisq.core.util.AveragePriceUtil; +import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinUtil; import bisq.network.p2p.P2PService; import bisq.common.app.Capabilities; +import bisq.common.app.Version; import bisq.common.util.MathUtils; - +import bisq.common.util.Utilities; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionInput; @@ -52,7 +55,7 @@ import javax.inject.Singleton; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.function.Predicate; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; @@ -95,6 +98,16 @@ public class OfferUtil { this.p2PService = p2PService; this.referralIdService = referralIdService; } + + public static String getRandomOfferId() { + return Utilities.getRandomPrefix(5, 8) + "-" + + UUID.randomUUID() + "-" + + getStrippedVersion(); + } + + public static String getStrippedVersion() { + return Version.VERSION.replace(".", ""); + } /** * Given the direction, is this a BUY? @@ -103,13 +116,13 @@ public class OfferUtil { * @return {@code true} for an offer to buy BTC from the taker, {@code false} for an * offer to sell BTC to the taker */ - public boolean isBuyOffer(Direction direction) { - return direction == Direction.BUY; + public boolean isBuyOffer(OfferDirection direction) { + return direction == OfferDirection.BUY; } public long getMaxTradeLimit(PaymentAccount paymentAccount, String currencyCode, - Direction direction) { + OfferDirection direction) { return paymentAccount != null ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction) : 0; @@ -148,7 +161,7 @@ public class OfferUtil { return volumeAsDouble / amountAsDouble; } - public double calculateMarketPriceMargin(double manualPrice, double marketPrice) { + public double calculateMarketPriceMarginPct(double manualPrice, double marketPrice) { return MathUtils.roundDouble(manualPrice / marketPrice, 4); } @@ -188,10 +201,11 @@ public class OfferUtil { } public boolean isBlockChainPaymentMethod(Offer offer) { - return offer != null && offer.getPaymentMethod().isAsset(); + return offer != null && offer.getPaymentMethod().isBlockchain(); } - public Optional getFeeInUserFiatCurrency(Coin makerFee) { + public Optional getFeeInUserFiatCurrency(Coin makerFee, + CoinFormatter formatter) { String userCurrencyCode = preferences.getPreferredTradeCurrency().getCode(); if (CurrencyUtil.isCryptoCurrency(userCurrencyCode)) { // In case the user has selected a altcoin as preferredTradeCurrency @@ -200,12 +214,14 @@ public class OfferUtil { userCurrencyCode = CurrencyUtil.getCurrencyByCountryCode(countryCode).getCode(); } - return getFeeInUserFiatCurrency(makerFee, userCurrencyCode); + return getFeeInUserFiatCurrency(makerFee, + userCurrencyCode, + formatter); } public Map getExtraDataMap(PaymentAccount paymentAccount, String currencyCode, - Direction direction) { + OfferDirection direction) { Map extraDataMap = new HashMap<>(); if (CurrencyUtil.isFiatCurrency(currencyCode)) { String myWitnessHashAsHex = accountAgeWitnessService @@ -228,7 +244,7 @@ public class OfferUtil { extraDataMap.put(CAPABILITIES, Capabilities.app.toStringList()); - if (currencyCode.equals("XMR") && direction == Direction.SELL) { + if (currencyCode.equals("XMR") && direction == OfferDirection.SELL) { preferences.getAutoConfirmSettingsList().stream() .filter(e -> e.getCurrencyCode().equals("XMR")) .filter(AutoConfirmSettings::isEnabled) @@ -256,12 +272,10 @@ public class OfferUtil { Res.get("offerbook.warning.paymentMethodBanned")); } - private Optional getFeeInUserFiatCurrency(Coin makerFee, - String userCurrencyCode) { + private Optional getFeeInUserFiatCurrency(Coin makerFee, String userCurrencyCode, CoinFormatter formatter) { MarketPrice marketPrice = priceFeedService.getMarketPrice(userCurrencyCode); if (marketPrice != null && makerFee != null) { - long marketPriceAsLong = roundDoubleToLong( - scaleUpByPowerOf10(marketPrice.getPrice(), Fiat.SMALLEST_UNIT_EXPONENT)); + long marketPriceAsLong = roundDoubleToLong(scaleUpByPowerOf10(marketPrice.getPrice(), Fiat.SMALLEST_UNIT_EXPONENT)); Price userCurrencyPrice = Price.valueOf(userCurrencyCode, marketPriceAsLong); return Optional.of(userCurrencyPrice.getVolumeByAmount(makerFee)); } else { @@ -269,6 +283,14 @@ public class OfferUtil { } } + public static boolean isFiatOffer(Offer offer) { + return offer.getBaseCurrencyCode().equals("XMR"); + } + + public static boolean isAltcoinOffer(Offer offer) { + return offer.getCounterCurrencyCode().equals("XMR"); + } + public static Optional getInvalidMakerFeeTxErrorMessage(Offer offer, BtcWalletService btcWalletService) { String offerFeePaymentTxId = offer.getOfferFeePaymentTxId(); if (offerFeePaymentTxId == null) { diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index ccc15bf025..806200454f 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -37,13 +37,14 @@ import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.mediation.mediator.Mediator; import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.TradableList; import bisq.core.trade.TradeUtils; -import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.handlers.TransactionResultHandler; import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.user.Preferences; import bisq.core.user.User; +import bisq.core.util.JsonUtil; import bisq.core.util.ParsingUtils; import bisq.core.util.Validator; @@ -224,9 +225,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe cleanUpAddressEntries(); - openOffers.stream() - .forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService) - .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg)))); + // TODO: add to invalid offers on failure +// openOffers.stream() +// .forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService) +// .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg)))); // process unposted offers processUnpostedOffers((transaction) -> {}, (errMessage) -> { @@ -823,7 +825,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // verify maker's reserve tx (double spend, trade fee, trade amount, mining fee) Offer offer = new Offer(request.getOfferPayload()); BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee()); - BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(offer.getDirection() == OfferPayload.Direction.BUY ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit())); + BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit())); xmrWalletService.verifyTradeTx( request.getPayoutAddress(), depositAmount, @@ -835,7 +837,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe true); // arbitrator signs offer to certify they have valid reserve tx - String offerPayloadAsJson = Utilities.objectToJson(request.getOfferPayload()); + String offerPayloadAsJson = JsonUtil.objectToJson(request.getOfferPayload()); String signature = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), offerPayloadAsJson); OfferPayload signedOfferPayload = request.getOfferPayload(); signedOfferPayload.setArbitratorSignature(signature); @@ -962,14 +964,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe openOffer.setBackupArbitrator(backupArbitratorNodeAddress); // maker signs taker's request // TODO (woodser): should maker signature include selected arbitrator? - String tradeRequestAsJson = Utilities.objectToJson(request.getTradeRequest()); + String tradeRequestAsJson = JsonUtil.objectToJson(request.getTradeRequest()); makerSignature = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), tradeRequestAsJson); try { // Check also tradePrice to avoid failures after taker fee is paid caused by a too big difference // in trade price between the peers. Also here poor connectivity might cause market price API connection // losses and therefore an outdated market price. - offer.checkTradePriceTolerance(request.getTakersTradePrice()); + offer.verifyTakersTradePrice(request.getTakersTradePrice()); availabilityResult = AvailabilityResult.AVAILABLE; } catch (TradePriceOutOfToleranceException e) { log.warn("Trade price check failed because takers price is outside out tolerance."); @@ -1155,7 +1157,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe originalOfferPayload.getPubKeyRing(), originalOfferPayload.getDirection(), originalOfferPayload.getPrice(), - originalOfferPayload.getMarketPriceMargin(), + originalOfferPayload.getMarketPriceMarginPct(), originalOfferPayload.isUseMarketBasedPrice(), originalOfferPayload.getAmount(), originalOfferPayload.getMinAmount(), diff --git a/core/src/main/java/bisq/core/offer/TriggerPriceService.java b/core/src/main/java/bisq/core/offer/TriggerPriceService.java index e665ed4e5b..253a63efb2 100644 --- a/core/src/main/java/bisq/core/offer/TriggerPriceService.java +++ b/core/src/main/java/bisq/core/offer/TriggerPriceService.java @@ -127,8 +127,8 @@ public class TriggerPriceService { return false; } - OfferPayload.Direction direction = openOffer.getOffer().getDirection(); - boolean isSellOffer = direction == OfferPayload.Direction.SELL; + OfferDirection direction = openOffer.getOffer().getDirection(); + boolean isSellOffer = direction == OfferDirection.SELL; boolean condition = isSellOffer && !cryptoCurrency || !isSellOffer && cryptoCurrency; return condition ? marketPriceAsLong < triggerPrice : diff --git a/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java b/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java index 1d4666fd84..ad5e78819e 100644 --- a/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java +++ b/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java @@ -46,7 +46,7 @@ import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import static bisq.core.btc.model.AddressEntry.Context.OFFER_FUNDING; -import static bisq.core.offer.OfferPayload.Direction.SELL; +import static bisq.core.offer.OfferDirection.SELL; import static bisq.core.util.VolumeUtil.getAdjustedVolumeForHalCash; import static bisq.core.util.VolumeUtil.getRoundedFiatVolume; import static bisq.core.util.coin.CoinUtil.minCoin; diff --git a/core/src/main/java/bisq/core/payment/AchTransferAccount.java b/core/src/main/java/bisq/core/payment/AchTransferAccount.java new file mode 100644 index 0000000000..a5bcb871be --- /dev/null +++ b/core/src/main/java/bisq/core/payment/AchTransferAccount.java @@ -0,0 +1,76 @@ +/* + * 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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.AchTransferAccountPayload; +import bisq.core.payment.payload.BankAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import java.util.List; + +import lombok.EqualsAndHashCode; +import lombok.NonNull; + +@EqualsAndHashCode(callSuper = true) +public final class AchTransferAccount extends CountryBasedPaymentAccount implements SameCountryRestrictedBankAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("USD")); + + public AchTransferAccount() { + super(PaymentMethod.ACH_TRANSFER); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new AchTransferAccountPayload(paymentMethod.getId(), id); + } + + @Override + public String getBankId() { + return ((BankAccountPayload) paymentAccountPayload).getBankId(); + } + + @Override + public String getCountryCode() { + return getCountry() != null ? getCountry().code : ""; + } + + public AchTransferAccountPayload getPayload() { + return (AchTransferAccountPayload) paymentAccountPayload; + } + + public String getMessageForBuyer() { + return "payment.achTransfer.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.achTransfer.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.achTransfer.info.account"; + } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/AdvancedCashAccount.java b/core/src/main/java/bisq/core/payment/AdvancedCashAccount.java index 172605f2eb..62632a5c05 100644 --- a/core/src/main/java/bisq/core/payment/AdvancedCashAccount.java +++ b/core/src/main/java/bisq/core/payment/AdvancedCashAccount.java @@ -17,18 +17,33 @@ package bisq.core.payment; -import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.AdvancedCashAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + import lombok.EqualsAndHashCode; +import org.jetbrains.annotations.NotNull; + @EqualsAndHashCode(callSuper = true) public final class AdvancedCashAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of( + new FiatCurrency("BRL"), + new FiatCurrency("EUR"), + new FiatCurrency("GBP"), + new FiatCurrency("KZT"), + new FiatCurrency("RUB"), + new FiatCurrency("UAH"), + new FiatCurrency("USD")); + public AdvancedCashAccount() { super(PaymentMethod.ADVANCED_CASH); - tradeCurrencies.addAll(CurrencyUtil.getAllAdvancedCashCurrencies()); + tradeCurrencies.addAll(SUPPORTED_CURRENCIES); } @Override @@ -36,6 +51,12 @@ public final class AdvancedCashAccount extends PaymentAccount { return new AdvancedCashAccountPayload(paymentMethod.getId(), id); } + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setAccountNr(String accountNr) { ((AdvancedCashAccountPayload) paymentAccountPayload).setAccountNr(accountNr); } diff --git a/core/src/main/java/bisq/core/payment/AliPayAccount.java b/core/src/main/java/bisq/core/payment/AliPayAccount.java index 6a2a49a990..ab3d724f8f 100644 --- a/core/src/main/java/bisq/core/payment/AliPayAccount.java +++ b/core/src/main/java/bisq/core/payment/AliPayAccount.java @@ -18,17 +18,24 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.AliPayAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class AliPayAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("CNY")); + public AliPayAccount() { super(PaymentMethod.ALI_PAY); - setSingleTradeCurrency(new FiatCurrency("CNY")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -36,6 +43,11 @@ public final class AliPayAccount extends PaymentAccount { return new AliPayAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setAccountNr(String accountNr) { ((AliPayAccountPayload) paymentAccountPayload).setAccountNr(accountNr); } diff --git a/core/src/main/java/bisq/core/payment/AmazonGiftCardAccount.java b/core/src/main/java/bisq/core/payment/AmazonGiftCardAccount.java index 76043b1bd5..52685b1674 100644 --- a/core/src/main/java/bisq/core/payment/AmazonGiftCardAccount.java +++ b/core/src/main/java/bisq/core/payment/AmazonGiftCardAccount.java @@ -19,16 +19,34 @@ package bisq.core.payment; import bisq.core.locale.Country; import bisq.core.locale.CountryUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.AmazonGiftCardAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; public final class AmazonGiftCardAccount extends PaymentAccount { + public static final List SUPPORTED_CURRENCIES = List.of( + new FiatCurrency("AUD"), + new FiatCurrency("CAD"), + new FiatCurrency("EUR"), + new FiatCurrency("GBP"), + new FiatCurrency("INR"), + new FiatCurrency("JPY"), + new FiatCurrency("SAR"), + new FiatCurrency("SEK"), + new FiatCurrency("SGD"), + new FiatCurrency("TRY"), + new FiatCurrency("USD") + ); + @Nullable private Country country; @@ -41,6 +59,12 @@ public final class AmazonGiftCardAccount extends PaymentAccount { return new AmazonGiftCardAccountPayload(paymentMethod.getId(), id); } + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public String getEmailOrMobileNr() { return getAmazonGiftCardAccountPayload().getEmailOrMobileNr(); } diff --git a/core/src/main/java/bisq/core/payment/AustraliaPayid.java b/core/src/main/java/bisq/core/payment/AustraliaPayidAccount.java similarity index 57% rename from core/src/main/java/bisq/core/payment/AustraliaPayid.java rename to core/src/main/java/bisq/core/payment/AustraliaPayidAccount.java index 537c1bc83b..3eab1302c6 100644 --- a/core/src/main/java/bisq/core/payment/AustraliaPayid.java +++ b/core/src/main/java/bisq/core/payment/AustraliaPayidAccount.java @@ -18,36 +18,49 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; -import bisq.core.payment.payload.AustraliaPayidPayload; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.AustraliaPayidAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; -public final class AustraliaPayid extends PaymentAccount { - public AustraliaPayid() { +import java.util.List; + +import lombok.NonNull; + +public final class AustraliaPayidAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("AUD")); + + public AustraliaPayidAccount() { super(PaymentMethod.AUSTRALIA_PAYID); - setSingleTradeCurrency(new FiatCurrency("AUD")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { - return new AustraliaPayidPayload(paymentMethod.getId(), id); + return new AustraliaPayidAccountPayload(paymentMethod.getId(), id); + } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; } public String getPayid() { - return ((AustraliaPayidPayload) paymentAccountPayload).getPayid(); + return ((AustraliaPayidAccountPayload) paymentAccountPayload).getPayid(); } public void setPayid(String payid) { if (payid == null) payid = ""; - ((AustraliaPayidPayload) paymentAccountPayload).setPayid(payid); + ((AustraliaPayidAccountPayload) paymentAccountPayload).setPayid(payid); } public String getBankAccountName() { - return ((AustraliaPayidPayload) paymentAccountPayload).getBankAccountName(); + return ((AustraliaPayidAccountPayload) paymentAccountPayload).getBankAccountName(); } public void setBankAccountName(String bankAccountName) { if (bankAccountName == null) bankAccountName = ""; - ((AustraliaPayidPayload) paymentAccountPayload).setBankAccountName(bankAccountName); + ((AustraliaPayidAccountPayload) paymentAccountPayload).setBankAccountName(bankAccountName); } } diff --git a/core/src/main/java/bisq/core/payment/BizumAccount.java b/core/src/main/java/bisq/core/payment/BizumAccount.java new file mode 100644 index 0000000000..d98cb3a41f --- /dev/null +++ b/core/src/main/java/bisq/core/payment/BizumAccount.java @@ -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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.BizumAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import java.util.List; + +import lombok.EqualsAndHashCode; +import lombok.NonNull; + +@EqualsAndHashCode(callSuper = true) +public final class BizumAccount extends CountryBasedPaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("EUR")); + + public BizumAccount() { + super(PaymentMethod.BIZUM); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new BizumAccountPayload(paymentMethod.getId(), id); + } + + public void setMobileNr(String mobileNr) { + ((BizumAccountPayload) paymentAccountPayload).setMobileNr(mobileNr); + } + + public String getMobileNr() { + return ((BizumAccountPayload) paymentAccountPayload).getMobileNr(); + } + + public String getMessageForBuyer() { + return "payment.bizum.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.bizum.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.bizum.info.account"; + } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/CapitualAccount.java b/core/src/main/java/bisq/core/payment/CapitualAccount.java new file mode 100644 index 0000000000..db827e9968 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/CapitualAccount.java @@ -0,0 +1,65 @@ +/* + * 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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.CapitualAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import java.util.List; + +import lombok.EqualsAndHashCode; + +import org.jetbrains.annotations.NotNull; + +@EqualsAndHashCode(callSuper = true) +public final class CapitualAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of( + new FiatCurrency("BRL"), + new FiatCurrency("EUR"), + new FiatCurrency("GBP"), + new FiatCurrency("USD") + ); + + public CapitualAccount() { + super(PaymentMethod.CAPITUAL); + tradeCurrencies.addAll(SUPPORTED_CURRENCIES); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new CapitualAccountPayload(paymentMethod.getId(), id); + } + + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + + public void setAccountNr(String accountNr) { + ((CapitualAccountPayload) paymentAccountPayload).setAccountNr(accountNr); + } + + public String getAccountNr() { + return ((CapitualAccountPayload) paymentAccountPayload).getAccountNr(); + } +} diff --git a/core/src/main/java/bisq/core/payment/CashAppAccount.java b/core/src/main/java/bisq/core/payment/CashAppAccount.java index 8a420cbb45..74c9951bbf 100644 --- a/core/src/main/java/bisq/core/payment/CashAppAccount.java +++ b/core/src/main/java/bisq/core/payment/CashAppAccount.java @@ -18,20 +18,27 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.CashAppAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; // Removed due too high chargeback risk // Cannot be deleted as it would break old trade history entries @Deprecated @EqualsAndHashCode(callSuper = true) public final class CashAppAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("USD")); + public CashAppAccount() { super(PaymentMethod.CASH_APP); - setSingleTradeCurrency(new FiatCurrency("USD")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -39,6 +46,11 @@ public final class CashAppAccount extends PaymentAccount { return new CashAppAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setCashTag(String cashTag) { ((CashAppAccountPayload) paymentAccountPayload).setCashTag(cashTag); } diff --git a/core/src/main/java/bisq/core/payment/CashByMailAccount.java b/core/src/main/java/bisq/core/payment/CashByMailAccount.java index 4ae88146ab..14d1da9e26 100644 --- a/core/src/main/java/bisq/core/payment/CashByMailAccount.java +++ b/core/src/main/java/bisq/core/payment/CashByMailAccount.java @@ -17,12 +17,20 @@ package bisq.core.payment; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.CashByMailAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + +import lombok.NonNull; + public final class CashByMailAccount extends PaymentAccount { + public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllFiatCurrencies(); + public CashByMailAccount() { super(PaymentMethod.CASH_BY_MAIL); } @@ -32,6 +40,11 @@ public final class CashByMailAccount extends PaymentAccount { return new CashByMailAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setPostalAddress(String postalAddress) { ((CashByMailAccountPayload) paymentAccountPayload).setPostalAddress(postalAddress); } diff --git a/core/src/main/java/bisq/core/payment/CashDepositAccount.java b/core/src/main/java/bisq/core/payment/CashDepositAccount.java index 3317e033b4..5113e34f34 100644 --- a/core/src/main/java/bisq/core/payment/CashDepositAccount.java +++ b/core/src/main/java/bisq/core/payment/CashDepositAccount.java @@ -17,13 +17,22 @@ package bisq.core.payment; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.CashDepositAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + +import lombok.NonNull; + import javax.annotation.Nullable; public final class CashDepositAccount extends CountryBasedPaymentAccount implements SameCountryRestrictedBankAccount { + + public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllFiatCurrencies(); + public CashDepositAccount() { super(PaymentMethod.CASH_DEPOSIT); } @@ -33,6 +42,11 @@ public final class CashDepositAccount extends CountryBasedPaymentAccount impleme return new CashDepositAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + @Override public String getBankId() { return ((CashDepositAccountPayload) paymentAccountPayload).getBankId(); diff --git a/core/src/main/java/bisq/core/payment/CelPayAccount.java b/core/src/main/java/bisq/core/payment/CelPayAccount.java new file mode 100644 index 0000000000..a33107028e --- /dev/null +++ b/core/src/main/java/bisq/core/payment/CelPayAccount.java @@ -0,0 +1,78 @@ +/* + * 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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.CelPayAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import java.util.List; + +import lombok.EqualsAndHashCode; + +import org.jetbrains.annotations.NotNull; + +@EqualsAndHashCode(callSuper = true) +public final class CelPayAccount extends PaymentAccount { + + // https://github.com/bisq-network/growth/issues/231 + public static final List SUPPORTED_CURRENCIES = List.of( + new FiatCurrency("AUD"), + new FiatCurrency("CAD"), + new FiatCurrency("GBP"), + new FiatCurrency("HKD"), + new FiatCurrency("USD") + ); + + public CelPayAccount() { + super(PaymentMethod.CELPAY); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new CelPayAccountPayload(paymentMethod.getId(), id); + } + + public void setEmail(String accountId) { + ((CelPayAccountPayload) paymentAccountPayload).setEmail(accountId); + } + + public String getEmail() { + return ((CelPayAccountPayload) paymentAccountPayload).getEmail(); + } + + public String getMessageForBuyer() { + return "payment.celpay.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.celpay.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.celpay.info.account"; + } + + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/ChaseQuickPayAccount.java b/core/src/main/java/bisq/core/payment/ChaseQuickPayAccount.java index daee039698..4a80f7d7cd 100644 --- a/core/src/main/java/bisq/core/payment/ChaseQuickPayAccount.java +++ b/core/src/main/java/bisq/core/payment/ChaseQuickPayAccount.java @@ -18,17 +18,27 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.ChaseQuickPayAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; -import lombok.EqualsAndHashCode; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.NonNull; + +// Removed due to QuickPay becoming Zelle +// Cannot be deleted as it would break old trade history entries +@Deprecated @EqualsAndHashCode(callSuper = true) public final class ChaseQuickPayAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("USD")); + public ChaseQuickPayAccount() { super(PaymentMethod.CHASE_QUICK_PAY); - setSingleTradeCurrency(new FiatCurrency("USD")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -36,6 +46,11 @@ public final class ChaseQuickPayAccount extends PaymentAccount { return new ChaseQuickPayAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setEmail(String email) { ((ChaseQuickPayAccountPayload) paymentAccountPayload).setEmail(email); } diff --git a/core/src/main/java/bisq/core/payment/ClearXchangeAccount.java b/core/src/main/java/bisq/core/payment/ClearXchangeAccount.java index 59e1aea1d2..6c99446bff 100644 --- a/core/src/main/java/bisq/core/payment/ClearXchangeAccount.java +++ b/core/src/main/java/bisq/core/payment/ClearXchangeAccount.java @@ -18,17 +18,24 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.ClearXchangeAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class ClearXchangeAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("USD")); + public ClearXchangeAccount() { super(PaymentMethod.CLEAR_X_CHANGE); - setSingleTradeCurrency(new FiatCurrency("USD")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -36,6 +43,11 @@ public final class ClearXchangeAccount extends PaymentAccount { return new ClearXchangeAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setEmailOrMobileNr(String mobileNr) { ((ClearXchangeAccountPayload) paymentAccountPayload).setEmailOrMobileNr(mobileNr); } diff --git a/core/src/main/java/bisq/core/payment/CryptoCurrencyAccount.java b/core/src/main/java/bisq/core/payment/CryptoCurrencyAccount.java index 69bec5ab17..8e11cf5e1a 100644 --- a/core/src/main/java/bisq/core/payment/CryptoCurrencyAccount.java +++ b/core/src/main/java/bisq/core/payment/CryptoCurrencyAccount.java @@ -17,15 +17,23 @@ package bisq.core.payment; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.CryptoCurrencyAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.ArrayList; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class CryptoCurrencyAccount extends AssetAccount { + public static final List SUPPORTED_CURRENCIES = new ArrayList<>(CurrencyUtil.getAllSortedCryptoCurrencies()); + public CryptoCurrencyAccount() { super(PaymentMethod.BLOCK_CHAINS); } @@ -34,4 +42,9 @@ public final class CryptoCurrencyAccount extends AssetAccount { protected PaymentAccountPayload createPayload() { return new CryptoCurrencyAccountPayload(paymentMethod.getId(), id); } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } } diff --git a/core/src/main/java/bisq/core/payment/DomesticWireTransferAccount.java b/core/src/main/java/bisq/core/payment/DomesticWireTransferAccount.java new file mode 100644 index 0000000000..a976f9ba14 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/DomesticWireTransferAccount.java @@ -0,0 +1,76 @@ +/* + * 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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.BankAccountPayload; +import bisq.core.payment.payload.DomesticWireTransferAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import java.util.List; + +import lombok.EqualsAndHashCode; +import lombok.NonNull; + +@EqualsAndHashCode(callSuper = true) +public final class DomesticWireTransferAccount extends CountryBasedPaymentAccount implements SameCountryRestrictedBankAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("USD")); + + public DomesticWireTransferAccount() { + super(PaymentMethod.DOMESTIC_WIRE_TRANSFER); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new DomesticWireTransferAccountPayload(paymentMethod.getId(), id); + } + + @Override + public String getBankId() { + return ((BankAccountPayload) paymentAccountPayload).getBankId(); + } + + @Override + public String getCountryCode() { + return getCountry() != null ? getCountry().code : ""; + } + + public DomesticWireTransferAccountPayload getPayload() { + return (DomesticWireTransferAccountPayload) paymentAccountPayload; + } + + public String getMessageForBuyer() { + return "payment.domesticWire.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.domesticWire.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.domesticWire.info.account"; + } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/F2FAccount.java b/core/src/main/java/bisq/core/payment/F2FAccount.java index a5723d3c16..4d6e9198fe 100644 --- a/core/src/main/java/bisq/core/payment/F2FAccount.java +++ b/core/src/main/java/bisq/core/payment/F2FAccount.java @@ -17,14 +17,22 @@ package bisq.core.payment; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.F2FAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class F2FAccount extends CountryBasedPaymentAccount { + + public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllFiatCurrencies(); + public F2FAccount() { super(PaymentMethod.F2F); } @@ -34,6 +42,11 @@ public final class F2FAccount extends CountryBasedPaymentAccount { return new F2FAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setContact(String contact) { ((F2FAccountPayload) paymentAccountPayload).setContact(contact); } diff --git a/core/src/main/java/bisq/core/payment/FasterPaymentsAccount.java b/core/src/main/java/bisq/core/payment/FasterPaymentsAccount.java index 94a6a32646..61b5385db4 100644 --- a/core/src/main/java/bisq/core/payment/FasterPaymentsAccount.java +++ b/core/src/main/java/bisq/core/payment/FasterPaymentsAccount.java @@ -18,17 +18,24 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.FasterPaymentsAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class FasterPaymentsAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("GBP")); + public FasterPaymentsAccount() { super(PaymentMethod.FASTER_PAYMENTS); - setSingleTradeCurrency(new FiatCurrency("GBP")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -36,6 +43,11 @@ public final class FasterPaymentsAccount extends PaymentAccount { return new FasterPaymentsAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setHolderName(String value) { ((FasterPaymentsAccountPayload) paymentAccountPayload).setHolderName(value); } diff --git a/core/src/main/java/bisq/core/payment/HalCashAccount.java b/core/src/main/java/bisq/core/payment/HalCashAccount.java index 9c105f69da..bf059b3441 100644 --- a/core/src/main/java/bisq/core/payment/HalCashAccount.java +++ b/core/src/main/java/bisq/core/payment/HalCashAccount.java @@ -18,17 +18,24 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.HalCashAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class HalCashAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("EUR")); + public HalCashAccount() { super(PaymentMethod.HAL_CASH); - setSingleTradeCurrency(new FiatCurrency("EUR")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -36,6 +43,11 @@ public final class HalCashAccount extends PaymentAccount { return new HalCashAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setMobileNr(String mobileNr) { ((HalCashAccountPayload) paymentAccountPayload).setMobileNr(mobileNr); } diff --git a/core/src/main/java/bisq/core/payment/IfscBasedAccount.java b/core/src/main/java/bisq/core/payment/IfscBasedAccount.java new file mode 100644 index 0000000000..316ea9df3b --- /dev/null +++ b/core/src/main/java/bisq/core/payment/IfscBasedAccount.java @@ -0,0 +1,40 @@ +/* + * 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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.PaymentMethod; + +import java.util.List; + +import lombok.NonNull; + +abstract public class IfscBasedAccount extends CountryBasedPaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("INR")); + + protected IfscBasedAccount(PaymentMethod paymentMethod) { + super(paymentMethod); + } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/ImpsAccount.java b/core/src/main/java/bisq/core/payment/ImpsAccount.java new file mode 100644 index 0000000000..b96b0ccaa6 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/ImpsAccount.java @@ -0,0 +1,61 @@ +/* + * 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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.ImpsAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import java.util.List; + +import lombok.EqualsAndHashCode; +import lombok.NonNull; + +@EqualsAndHashCode(callSuper = true) +public final class ImpsAccount extends CountryBasedPaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("INR")); + + public ImpsAccount() { + super(PaymentMethod.IMPS); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new ImpsAccountPayload(paymentMethod.getId(), id); + } + + public String getMessageForBuyer() { + return "payment.imps.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.imps.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.imps.info.account"; + } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/InstantCryptoCurrencyAccount.java b/core/src/main/java/bisq/core/payment/InstantCryptoCurrencyAccount.java index 176a155ec7..4fd6cb9ae2 100644 --- a/core/src/main/java/bisq/core/payment/InstantCryptoCurrencyAccount.java +++ b/core/src/main/java/bisq/core/payment/InstantCryptoCurrencyAccount.java @@ -17,15 +17,23 @@ package bisq.core.payment; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.InstantCryptoCurrencyPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.ArrayList; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class InstantCryptoCurrencyAccount extends AssetAccount { + public static final List SUPPORTED_CURRENCIES = new ArrayList<>(CurrencyUtil.getAllSortedCryptoCurrencies()); + public InstantCryptoCurrencyAccount() { super(PaymentMethod.BLOCK_CHAINS_INSTANT); } @@ -34,4 +42,9 @@ public final class InstantCryptoCurrencyAccount extends AssetAccount { protected PaymentAccountPayload createPayload() { return new InstantCryptoCurrencyPayload(paymentMethod.getId(), id); } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } } diff --git a/core/src/main/java/bisq/core/payment/InteracETransferAccount.java b/core/src/main/java/bisq/core/payment/InteracETransferAccount.java index 4e716b2421..00db851ce9 100644 --- a/core/src/main/java/bisq/core/payment/InteracETransferAccount.java +++ b/core/src/main/java/bisq/core/payment/InteracETransferAccount.java @@ -18,17 +18,25 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.InteracETransferAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + import lombok.EqualsAndHashCode; +import org.jetbrains.annotations.NotNull; + @EqualsAndHashCode(callSuper = true) public final class InteracETransferAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("CAD")); + public InteracETransferAccount() { super(PaymentMethod.INTERAC_E_TRANSFER); - setSingleTradeCurrency(new FiatCurrency("CAD")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -36,6 +44,12 @@ public final class InteracETransferAccount extends PaymentAccount { return new InteracETransferAccountPayload(paymentMethod.getId(), id); } + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setEmail(String email) { ((InteracETransferAccountPayload) paymentAccountPayload).setEmail(email); } diff --git a/core/src/main/java/bisq/core/payment/JapanBankAccount.java b/core/src/main/java/bisq/core/payment/JapanBankAccount.java index 3948c59cf5..9f38fddb61 100644 --- a/core/src/main/java/bisq/core/payment/JapanBankAccount.java +++ b/core/src/main/java/bisq/core/payment/JapanBankAccount.java @@ -18,97 +18,100 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.JapanBankAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; -public final class JapanBankAccount extends PaymentAccount -{ - public JapanBankAccount() - { +import java.util.List; + +import lombok.NonNull; + +public final class JapanBankAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("JPY")); + + public JapanBankAccount() { super(PaymentMethod.JAPAN_BANK); - setSingleTradeCurrency(new FiatCurrency("JPY")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override - protected PaymentAccountPayload createPayload() - { + protected PaymentAccountPayload createPayload() { return new JapanBankAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + // bank code - public String getBankCode() - { + public String getBankCode() { return ((JapanBankAccountPayload) paymentAccountPayload).getBankCode(); } - public void setBankCode(String bankCode) - { + + public void setBankCode(String bankCode) { if (bankCode == null) bankCode = ""; ((JapanBankAccountPayload) paymentAccountPayload).setBankCode(bankCode); } // bank name - public String getBankName() - { + public String getBankName() { return ((JapanBankAccountPayload) paymentAccountPayload).getBankName(); } - public void setBankName(String bankName) - { + + public void setBankName(String bankName) { if (bankName == null) bankName = ""; ((JapanBankAccountPayload) paymentAccountPayload).setBankName(bankName); } // branch code - public String getBankBranchCode() - { + public String getBankBranchCode() { return ((JapanBankAccountPayload) paymentAccountPayload).getBankBranchCode(); } - public void setBankBranchCode(String bankBranchCode) - { + + public void setBankBranchCode(String bankBranchCode) { if (bankBranchCode == null) bankBranchCode = ""; ((JapanBankAccountPayload) paymentAccountPayload).setBankBranchCode(bankBranchCode); } // branch name - public String getBankBranchName() - { + public String getBankBranchName() { return ((JapanBankAccountPayload) paymentAccountPayload).getBankBranchName(); } - public void setBankBranchName(String bankBranchName) - { + + public void setBankBranchName(String bankBranchName) { if (bankBranchName == null) bankBranchName = ""; ((JapanBankAccountPayload) paymentAccountPayload).setBankBranchName(bankBranchName); } // account type - public String getBankAccountType() - { + public String getBankAccountType() { return ((JapanBankAccountPayload) paymentAccountPayload).getBankAccountType(); } - public void setBankAccountType(String bankAccountType) - { + + public void setBankAccountType(String bankAccountType) { if (bankAccountType == null) bankAccountType = ""; ((JapanBankAccountPayload) paymentAccountPayload).setBankAccountType(bankAccountType); } // account number - public String getBankAccountNumber() - { + public String getBankAccountNumber() { return ((JapanBankAccountPayload) paymentAccountPayload).getBankAccountNumber(); } - public void setBankAccountNumber(String bankAccountNumber) - { + + public void setBankAccountNumber(String bankAccountNumber) { if (bankAccountNumber == null) bankAccountNumber = ""; ((JapanBankAccountPayload) paymentAccountPayload).setBankAccountNumber(bankAccountNumber); } // account name - public String getBankAccountName() - { + public String getBankAccountName() { return ((JapanBankAccountPayload) paymentAccountPayload).getBankAccountName(); } - public void setBankAccountName(String bankAccountName) - { + + public void setBankAccountName(String bankAccountName) { if (bankAccountName == null) bankAccountName = ""; ((JapanBankAccountPayload) paymentAccountPayload).setBankAccountName(bankAccountName); } diff --git a/core/src/main/java/bisq/core/payment/MoneseAccount.java b/core/src/main/java/bisq/core/payment/MoneseAccount.java new file mode 100644 index 0000000000..aac0608f8b --- /dev/null +++ b/core/src/main/java/bisq/core/payment/MoneseAccount.java @@ -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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.MoneseAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import java.util.List; + +import lombok.EqualsAndHashCode; +import lombok.NonNull; + +@EqualsAndHashCode(callSuper = true) +public final class MoneseAccount extends PaymentAccount { + + // https://github.com/bisq-network/growth/issues/227 + public static final List SUPPORTED_CURRENCIES = List.of( + new FiatCurrency("EUR"), + new FiatCurrency("GBP"), + new FiatCurrency("RON") + ); + + public MoneseAccount() { + super(PaymentMethod.MONESE); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new MoneseAccountPayload(paymentMethod.getId(), id); + } + + public void setHolderName(String accountId) { + ((MoneseAccountPayload) paymentAccountPayload).setHolderName(accountId); + } + + public String getHolderName() { + return ((MoneseAccountPayload) paymentAccountPayload).getHolderName(); + } + + public void setMobileNr(String accountId) { + ((MoneseAccountPayload) paymentAccountPayload).setMobileNr(accountId); + } + + public String getMobileNr() { + return ((MoneseAccountPayload) paymentAccountPayload).getMobileNr(); + } + + public String getMessageForBuyer() { + return "payment.monese.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.monese.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.monese.info.account"; + } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/MoneyBeamAccount.java b/core/src/main/java/bisq/core/payment/MoneyBeamAccount.java index e40359c3f6..318168d229 100644 --- a/core/src/main/java/bisq/core/payment/MoneyBeamAccount.java +++ b/core/src/main/java/bisq/core/payment/MoneyBeamAccount.java @@ -18,18 +18,25 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.MoneyBeamAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; //TODO missing support for selected trade currency @EqualsAndHashCode(callSuper = true) public final class MoneyBeamAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("EUR")); + public MoneyBeamAccount() { super(PaymentMethod.MONEY_BEAM); - setSingleTradeCurrency(new FiatCurrency("EUR")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -37,6 +44,11 @@ public final class MoneyBeamAccount extends PaymentAccount { return new MoneyBeamAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setAccountId(String accountId) { ((MoneyBeamAccountPayload) paymentAccountPayload).setAccountId(accountId); } diff --git a/core/src/main/java/bisq/core/payment/MoneyGramAccount.java b/core/src/main/java/bisq/core/payment/MoneyGramAccount.java index 3ff4bb41b0..c27b100341 100644 --- a/core/src/main/java/bisq/core/payment/MoneyGramAccount.java +++ b/core/src/main/java/bisq/core/payment/MoneyGramAccount.java @@ -19,11 +19,14 @@ package bisq.core.payment; import bisq.core.locale.Country; import bisq.core.locale.CountryUtil; -import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.MoneyGramAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; @@ -36,10 +39,60 @@ public final class MoneyGramAccount extends PaymentAccount { @Nullable private Country country; + public static final List SUPPORTED_CURRENCIES = List.of( + new FiatCurrency("AED"), + new FiatCurrency("ARS"), + new FiatCurrency("AUD"), + new FiatCurrency("BND"), + new FiatCurrency("CAD"), + new FiatCurrency("CHF"), + new FiatCurrency("CZK"), + new FiatCurrency("DKK"), + new FiatCurrency("EUR"), + new FiatCurrency("FJD"), + new FiatCurrency("GBP"), + new FiatCurrency("HKD"), + new FiatCurrency("HUF"), + new FiatCurrency("IDR"), + new FiatCurrency("ILS"), + new FiatCurrency("INR"), + new FiatCurrency("JPY"), + new FiatCurrency("KRW"), + new FiatCurrency("KWD"), + new FiatCurrency("LKR"), + new FiatCurrency("MAD"), + new FiatCurrency("MGA"), + new FiatCurrency("MXN"), + new FiatCurrency("MYR"), + new FiatCurrency("NOK"), + new FiatCurrency("NZD"), + new FiatCurrency("OMR"), + new FiatCurrency("PEN"), + new FiatCurrency("PGK"), + new FiatCurrency("PHP"), + new FiatCurrency("PKR"), + new FiatCurrency("PLN"), + new FiatCurrency("SAR"), + new FiatCurrency("SBD"), + new FiatCurrency("SCR"), + new FiatCurrency("SEK"), + new FiatCurrency("SGD"), + new FiatCurrency("THB"), + new FiatCurrency("TOP"), + new FiatCurrency("TRY"), + new FiatCurrency("TWD"), + new FiatCurrency("USD"), + new FiatCurrency("VND"), + new FiatCurrency("VUV"), + new FiatCurrency("WST"), + new FiatCurrency("XOF"), + new FiatCurrency("XPF"), + new FiatCurrency("ZAR") + ); public MoneyGramAccount() { super(PaymentMethod.MONEY_GRAM); - tradeCurrencies.addAll(CurrencyUtil.getAllMoneyGramCurrencies()); + tradeCurrencies.addAll(SUPPORTED_CURRENCIES); } @Override @@ -47,6 +100,12 @@ public final class MoneyGramAccount extends PaymentAccount { return new MoneyGramAccountPayload(paymentMethod.getId(), id); } + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + @Nullable public Country getCountry() { if (country == null) { diff --git a/core/src/main/java/bisq/core/payment/NationalBankAccount.java b/core/src/main/java/bisq/core/payment/NationalBankAccount.java index 5d3533bf31..b4f3b8a6f8 100644 --- a/core/src/main/java/bisq/core/payment/NationalBankAccount.java +++ b/core/src/main/java/bisq/core/payment/NationalBankAccount.java @@ -17,15 +17,23 @@ package bisq.core.payment; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.BankAccountPayload; import bisq.core.payment.payload.NationalBankAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class NationalBankAccount extends CountryBasedPaymentAccount implements SameCountryRestrictedBankAccount { + + public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllFiatCurrencies(); + public NationalBankAccount() { super(PaymentMethod.NATIONAL_BANK); } @@ -35,6 +43,11 @@ public final class NationalBankAccount extends CountryBasedPaymentAccount implem return new NationalBankAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + @Override public String getBankId() { return ((BankAccountPayload) paymentAccountPayload).getBankId(); diff --git a/core/src/main/java/bisq/core/payment/NeftAccount.java b/core/src/main/java/bisq/core/payment/NeftAccount.java new file mode 100644 index 0000000000..781510ff93 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/NeftAccount.java @@ -0,0 +1,50 @@ +/* + * 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 . + */ + +package bisq.core.payment; + +import bisq.core.payment.payload.NeftAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class NeftAccount extends IfscBasedAccount { + + public NeftAccount() { + super(PaymentMethod.NEFT); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new NeftAccountPayload(paymentMethod.getId(), id); + } + + public String getMessageForBuyer() { + return "payment.neft.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.neft.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.neft.info.account"; + } + +} diff --git a/core/src/main/java/bisq/core/payment/NequiAccount.java b/core/src/main/java/bisq/core/payment/NequiAccount.java new file mode 100644 index 0000000000..30021dff6b --- /dev/null +++ b/core/src/main/java/bisq/core/payment/NequiAccount.java @@ -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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.NequiAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import java.util.List; + +import lombok.EqualsAndHashCode; +import lombok.NonNull; + +@EqualsAndHashCode(callSuper = true) +public final class NequiAccount extends CountryBasedPaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("COP")); + + public NequiAccount() { + super(PaymentMethod.NEQUI); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new NequiAccountPayload(paymentMethod.getId(), id); + } + + public void setMobileNr(String mobileNr) { + ((NequiAccountPayload) paymentAccountPayload).setMobileNr(mobileNr); + } + + public String getMobileNr() { + return ((NequiAccountPayload) paymentAccountPayload).getMobileNr(); + } + + public String getMessageForBuyer() { + return "payment.nequi.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.nequi.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.nequi.info.account"; + } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/OKPayAccount.java b/core/src/main/java/bisq/core/payment/OKPayAccount.java index 42207db6ae..448d2c47b8 100644 --- a/core/src/main/java/bisq/core/payment/OKPayAccount.java +++ b/core/src/main/java/bisq/core/payment/OKPayAccount.java @@ -17,22 +17,53 @@ package bisq.core.payment; -import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.OKPayAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import java.util.List; + import lombok.EqualsAndHashCode; +import org.jetbrains.annotations.NotNull; + // Cannot be deleted as it would break old trade history entries @Deprecated @EqualsAndHashCode(callSuper = true) public final class OKPayAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of( + new FiatCurrency("AED"), + new FiatCurrency("ARS"), + new FiatCurrency("AUD"), + new FiatCurrency("BRL"), + new FiatCurrency("CAD"), + new FiatCurrency("CHF"), + new FiatCurrency("CNY"), + new FiatCurrency("DKK"), + new FiatCurrency("EUR"), + new FiatCurrency("GBP"), + new FiatCurrency("HKD"), + new FiatCurrency("ILS"), + new FiatCurrency("INR"), + new FiatCurrency("JPY"), + new FiatCurrency("KES"), + new FiatCurrency("MXN"), + new FiatCurrency("NOK"), + new FiatCurrency("NZD"), + new FiatCurrency("PHP"), + new FiatCurrency("PLN"), + new FiatCurrency("SEK"), + new FiatCurrency("SGD"), + new FiatCurrency("USD") + ); + public OKPayAccount() { super(PaymentMethod.OK_PAY); - // Incorrect call but we don't want to keep Deprecated code in CurrencyUtil if not needed... - tradeCurrencies.addAll(CurrencyUtil.getAllUpholdCurrencies()); + tradeCurrencies.addAll(SUPPORTED_CURRENCIES); } @Override @@ -40,6 +71,12 @@ public final class OKPayAccount extends PaymentAccount { return new OKPayAccountPayload(paymentMethod.getId(), id); } + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setAccountNr(String accountNr) { ((OKPayAccountPayload) paymentAccountPayload).setAccountNr(accountNr); } diff --git a/core/src/main/java/bisq/core/payment/PaxumAccount.java b/core/src/main/java/bisq/core/payment/PaxumAccount.java new file mode 100644 index 0000000000..19e33f1001 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/PaxumAccount.java @@ -0,0 +1,79 @@ +/* + * 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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.PaxumAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import java.util.List; + +import lombok.EqualsAndHashCode; + +import org.jetbrains.annotations.NotNull; + +@EqualsAndHashCode(callSuper = true) +public final class PaxumAccount extends PaymentAccount { + + // https://github.com/bisq-network/growth/issues/235 + public static final List SUPPORTED_CURRENCIES = List.of( + new FiatCurrency("AUD"), + new FiatCurrency("CAD"), + new FiatCurrency("CHF"), + new FiatCurrency("CZK"), + new FiatCurrency("DKK"), + new FiatCurrency("EUR"), + new FiatCurrency("GBP"), + new FiatCurrency("HUF"), + new FiatCurrency("IDR"), + new FiatCurrency("INR"), + new FiatCurrency("NOK"), + new FiatCurrency("NZD"), + new FiatCurrency("PLN"), + new FiatCurrency("RON"), + new FiatCurrency("SEK"), + new FiatCurrency("THB"), + new FiatCurrency("USD"), + new FiatCurrency("ZAR") + ); + + public PaxumAccount() { + super(PaymentMethod.PAXUM); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new PaxumAccountPayload(paymentMethod.getId(), id); + } + + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + + public void setEmail(String accountId) { + ((PaxumAccountPayload) paymentAccountPayload).setEmail(accountId); + } + + public String getEmail() { + return ((PaxumAccountPayload) paymentAccountPayload).getEmail(); + } +} diff --git a/core/src/main/java/bisq/core/payment/PaymentAccount.java b/core/src/main/java/bisq/core/payment/PaymentAccount.java index 02a73cf87d..b162b3e1e0 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccount.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccount.java @@ -35,12 +35,14 @@ import java.util.stream.Collectors; import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.NonNull; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; +import static bisq.core.payment.payload.PaymentMethod.TRANSFERWISE_ID; import static com.google.common.base.Preconditions.checkNotNull; @EqualsAndHashCode @@ -48,6 +50,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Getter @Slf4j public abstract class PaymentAccount implements PersistablePayload { + protected final PaymentMethod paymentMethod; @Setter protected String id; @@ -57,6 +60,10 @@ public abstract class PaymentAccount implements PersistablePayload { public PaymentAccountPayload paymentAccountPayload; @Setter protected String accountName; + @Setter + @EqualsAndHashCode.Exclude + protected String persistedAccountName; + protected final List tradeCurrencies = new ArrayList<>(); @Setter @Nullable @@ -104,24 +111,30 @@ public abstract class PaymentAccount implements PersistablePayload { // We need to remove NGN for Transferwise Optional ngnTwOptional = tradeCurrencies.stream() - .filter(e -> paymentMethodId.equals(PaymentMethod.TRANSFERWISE_ID)) + .filter(e -> paymentMethodId.equals(TRANSFERWISE_ID)) .filter(e -> e.getCode().equals("NGN")) .findAny(); // We cannot remove it in the stream as it would cause a concurrentModificationException ngnTwOptional.ifPresent(tradeCurrencies::remove); - PaymentAccount account = PaymentAccountFactory.getPaymentAccount(PaymentMethod.getPaymentMethodById(paymentMethodId)); - account.getTradeCurrencies().clear(); - account.setId(proto.getId()); - account.setCreationDate(proto.getCreationDate()); - account.setAccountName(proto.getAccountName()); - account.getTradeCurrencies().addAll(tradeCurrencies); - account.setPaymentAccountPayload(coreProtoResolver.fromProto(proto.getPaymentAccountPayload())); + try { + PaymentAccount account = PaymentAccountFactory.getPaymentAccount(PaymentMethod.getPaymentMethod(paymentMethodId)); + account.getTradeCurrencies().clear(); + account.setId(proto.getId()); + account.setCreationDate(proto.getCreationDate()); + account.setAccountName(proto.getAccountName()); + account.setPersistedAccountName(proto.getAccountName()); + account.getTradeCurrencies().addAll(tradeCurrencies); + account.setPaymentAccountPayload(coreProtoResolver.fromProto(proto.getPaymentAccountPayload())); - if (proto.hasSelectedTradeCurrency()) - account.setSelectedTradeCurrency(TradeCurrency.fromProto(proto.getSelectedTradeCurrency())); + if (proto.hasSelectedTradeCurrency()) + account.setSelectedTradeCurrency(TradeCurrency.fromProto(proto.getSelectedTradeCurrency())); - return account; + return account; + } catch (RuntimeException e) { + log.warn("Could not load account: {}, exception: {}", paymentMethodId, e.toString()); + return null; + } } @@ -190,23 +203,15 @@ public abstract class PaymentAccount implements PersistablePayload { return this instanceof CountryBasedPaymentAccount; } - public boolean isHalCashAccount() { - return this instanceof HalCashAccount; - } - - public boolean isMoneyGramAccount() { - return this instanceof MoneyGramAccount; - } - - public boolean isTransferwiseAccount() { - return this instanceof TransferwiseAccount; + public boolean hasPaymentMethodWithId(String paymentMethodId) { + return this.getPaymentMethod().getId().equals(paymentMethodId); } /** * Return an Optional of the trade currency for this payment account, or * Optional.empty() if none is found. If this payment account has a selected * trade currency, that is returned, else its single trade currency is returned, - * else the first trade currency in the this payment account's tradeCurrencies + * else the first trade currency in this payment account's tradeCurrencies * list is returned. * * @return Optional of the trade currency for the given payment account @@ -226,4 +231,38 @@ public abstract class PaymentAccount implements PersistablePayload { // We are in the process to get added to the user. This is called just before saving the account and the // last moment we could apply some special handling if needed (e.g. as it happens for Revolut) } + + public String getPreTradeMessage(boolean isBuyer) { + if (isBuyer) { + return getMessageForBuyer(); + } else { + return getMessageForSeller(); + } + } + + // will be overridden by specific account when necessary + public String getMessageForBuyer() { + return null; + } + + // will be overridden by specific account when necessary + public String getMessageForSeller() { + return null; + } + + // will be overridden by specific account when necessary + public String getMessageForAccountCreation() { + return null; + } + + public void onPersistChanges() { + setPersistedAccountName(getAccountName()); + } + + public void revertChanges() { + setAccountName(getPersistedAccountName()); + } + + @NonNull + public abstract List getSupportedCurrencies(); } diff --git a/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java b/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java index d34f358b2b..2625c3a9b5 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java @@ -47,7 +47,7 @@ public class PaymentAccountFactory { case PaymentMethod.JAPAN_BANK_ID: return new JapanBankAccount(); case PaymentMethod.AUSTRALIA_PAYID_ID: - return new AustraliaPayid(); + return new AustraliaPayidAccount(); case PaymentMethod.ALI_PAY_ID: return new AliPayAccount(); case PaymentMethod.WECHAT_PAY_ID: @@ -82,10 +82,52 @@ public class PaymentAccountFactory { return new AdvancedCashAccount(); case PaymentMethod.TRANSFERWISE_ID: return new TransferwiseAccount(); + case PaymentMethod.TRANSFERWISE_USD_ID: + return new TransferwiseUsdAccount(); + case PaymentMethod.PAYSERA_ID: + return new PayseraAccount(); + case PaymentMethod.PAXUM_ID: + return new PaxumAccount(); + case PaymentMethod.NEFT_ID: + return new NeftAccount(); + case PaymentMethod.RTGS_ID: + return new RtgsAccount(); + case PaymentMethod.IMPS_ID: + return new ImpsAccount(); + case PaymentMethod.UPI_ID: + return new UpiAccount(); + case PaymentMethod.PAYTM_ID: + return new PaytmAccount(); + case PaymentMethod.NEQUI_ID: + return new NequiAccount(); + case PaymentMethod.BIZUM_ID: + return new BizumAccount(); + case PaymentMethod.PIX_ID: + return new PixAccount(); case PaymentMethod.AMAZON_GIFT_CARD_ID: return new AmazonGiftCardAccount(); case PaymentMethod.BLOCK_CHAINS_INSTANT_ID: return new InstantCryptoCurrencyAccount(); + case PaymentMethod.CAPITUAL_ID: + return new CapitualAccount(); + case PaymentMethod.CELPAY_ID: + return new CelPayAccount(); + case PaymentMethod.MONESE_ID: + return new MoneseAccount(); + case PaymentMethod.SATISPAY_ID: + return new SatispayAccount(); + case PaymentMethod.TIKKIE_ID: + return new TikkieAccount(); + case PaymentMethod.VERSE_ID: + return new VerseAccount(); + case PaymentMethod.STRIKE_ID: + return new StrikeAccount(); + case PaymentMethod.SWIFT_ID: + return new SwiftAccount(); + case PaymentMethod.ACH_TRANSFER_ID: + return new AchTransferAccount(); + case PaymentMethod.DOMESTIC_WIRE_TRANSFER_ID: + return new DomesticWireTransferAccount(); // Cannot be deleted as it would break old trade history entries case PaymentMethod.OK_PAY_ID: diff --git a/core/src/main/java/bisq/core/payment/PaymentAccountList.java b/core/src/main/java/bisq/core/payment/PaymentAccountList.java index 6e3ad1cea4..e25654e1ae 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccountList.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccountList.java @@ -25,6 +25,7 @@ import com.google.protobuf.Message; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import lombok.EqualsAndHashCode; @@ -47,6 +48,7 @@ public class PaymentAccountList extends PersistableList { public static PaymentAccountList fromProto(protobuf.PaymentAccountList proto, CoreProtoResolver coreProtoResolver) { return new PaymentAccountList(new ArrayList<>(proto.getPaymentAccountList().stream() .map(e -> PaymentAccount.fromProto(e, coreProtoResolver)) + .filter(Objects::nonNull) .collect(Collectors.toList()))); } } diff --git a/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java b/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java index 41b77ed3b5..17f7efb3b9 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java @@ -19,6 +19,7 @@ package bisq.core.payment; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.Country; +import bisq.core.locale.TradeCurrency; import bisq.core.offer.Offer; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; @@ -29,6 +30,7 @@ import javafx.collections.ObservableList; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; @@ -38,6 +40,8 @@ import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; +import static bisq.core.payment.payload.PaymentMethod.*; + @Slf4j public class PaymentAccountUtil { @@ -65,7 +69,7 @@ public class PaymentAccountUtil { public static boolean isAmountValidForOffer(Offer offer, PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService) { - boolean hasChargebackRisk = PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()); + boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()); boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount, offer.getCurrencyCode(), offer.getMirroredDirection()) >= offer.getMinAmount().value; return !hasChargebackRisk || hasValidAccountAgeWitness; @@ -106,6 +110,120 @@ public class PaymentAccountUtil { return acceptedCountryCodes; } + public static List getTradeCurrencies(PaymentMethod paymentMethod) { + switch (paymentMethod.getId()) { + case ADVANCED_CASH_ID: + return AdvancedCashAccount.SUPPORTED_CURRENCIES; + case AMAZON_GIFT_CARD_ID: + return AmazonGiftCardAccount.SUPPORTED_CURRENCIES; + case CAPITUAL_ID: + return CapitualAccount.SUPPORTED_CURRENCIES; + case MONEY_GRAM_ID: + return MoneyGramAccount.SUPPORTED_CURRENCIES; + case PAXUM_ID: + return PaxumAccount.SUPPORTED_CURRENCIES; + case PAYSERA_ID: + return PayseraAccount.SUPPORTED_CURRENCIES; + case REVOLUT_ID: + return RevolutAccount.SUPPORTED_CURRENCIES; + case SWIFT_ID: + return SwiftAccount.SUPPORTED_CURRENCIES; + case TRANSFERWISE_ID: + return TransferwiseAccount.SUPPORTED_CURRENCIES; + case UPHOLD_ID: + return UpholdAccount.SUPPORTED_CURRENCIES; + case INTERAC_E_TRANSFER_ID: + return InteracETransferAccount.SUPPORTED_CURRENCIES; + case STRIKE_ID: + return StrikeAccount.SUPPORTED_CURRENCIES; + case TIKKIE_ID: + return TikkieAccount.SUPPORTED_CURRENCIES; + case ALI_PAY_ID: + return AliPayAccount.SUPPORTED_CURRENCIES; + case NEQUI_ID: + return NequiAccount.SUPPORTED_CURRENCIES; + case IMPS_ID: + case NEFT_ID: + case PAYTM_ID: + case RTGS_ID: + case UPI_ID: + return IfscBasedAccount.SUPPORTED_CURRENCIES; + case BIZUM_ID: + return BizumAccount.SUPPORTED_CURRENCIES; + case MONEY_BEAM_ID: + return MoneyBeamAccount.SUPPORTED_CURRENCIES; + case PIX_ID: + return PixAccount.SUPPORTED_CURRENCIES; + case SATISPAY_ID: + return SatispayAccount.SUPPORTED_CURRENCIES; + case CHASE_QUICK_PAY_ID: + return ChaseQuickPayAccount.SUPPORTED_CURRENCIES; + case US_POSTAL_MONEY_ORDER_ID: + return USPostalMoneyOrderAccount.SUPPORTED_CURRENCIES; + case VENMO_ID: + return VenmoAccount.SUPPORTED_CURRENCIES; + case JAPAN_BANK_ID: + return JapanBankAccount.SUPPORTED_CURRENCIES; + case WECHAT_PAY_ID: + return WeChatPayAccount.SUPPORTED_CURRENCIES; + case CLEAR_X_CHANGE_ID: + return ClearXchangeAccount.SUPPORTED_CURRENCIES; + case AUSTRALIA_PAYID_ID: + return AustraliaPayidAccount.SUPPORTED_CURRENCIES; + case PERFECT_MONEY_ID: + return PerfectMoneyAccount.SUPPORTED_CURRENCIES; + case HAL_CASH_ID: + return HalCashAccount.SUPPORTED_CURRENCIES; + case SWISH_ID: + return SwishAccount.SUPPORTED_CURRENCIES; + case CASH_APP_ID: + return CashAppAccount.SUPPORTED_CURRENCIES; + case POPMONEY_ID: + return PopmoneyAccount.SUPPORTED_CURRENCIES; + case PROMPT_PAY_ID: + return PromptPayAccount.SUPPORTED_CURRENCIES; + case SEPA_ID: + return SepaAccount.SUPPORTED_CURRENCIES; + case SEPA_INSTANT_ID: + return SepaInstantAccount.SUPPORTED_CURRENCIES; + case CASH_BY_MAIL_ID: + return CashByMailAccount.SUPPORTED_CURRENCIES; + case F2F_ID: + return F2FAccount.SUPPORTED_CURRENCIES; + case NATIONAL_BANK_ID: + return NationalBankAccount.SUPPORTED_CURRENCIES; + case SAME_BANK_ID: + return SameBankAccount.SUPPORTED_CURRENCIES; + case SPECIFIC_BANKS_ID: + return SpecificBanksAccount.SUPPORTED_CURRENCIES; + case CASH_DEPOSIT_ID: + return CashDepositAccount.SUPPORTED_CURRENCIES; + case WESTERN_UNION_ID: + return WesternUnionAccount.SUPPORTED_CURRENCIES; + case FASTER_PAYMENTS_ID: + return FasterPaymentsAccount.SUPPORTED_CURRENCIES; + case DOMESTIC_WIRE_TRANSFER_ID: + return DomesticWireTransferAccount.SUPPORTED_CURRENCIES; + case ACH_TRANSFER_ID: + return AchTransferAccount.SUPPORTED_CURRENCIES; + case CELPAY_ID: + return CelPayAccount.SUPPORTED_CURRENCIES; + case MONESE_ID: + return MoneseAccount.SUPPORTED_CURRENCIES; + case TRANSFERWISE_USD_ID: + return TransferwiseUsdAccount.SUPPORTED_CURRENCIES; + case VERSE_ID: + return VerseAccount.SUPPORTED_CURRENCIES; + default: + return Collections.emptyList(); + } + } + + public static boolean supportsCurrency(PaymentMethod paymentMethod, TradeCurrency selectedTradeCurrency) { + return getTradeCurrencies(paymentMethod).stream() + .anyMatch(tradeCurrency -> tradeCurrency.equals(selectedTradeCurrency)); + } + @Nullable public static List getAcceptedBanks(PaymentAccount paymentAccount) { List acceptedBanks = null; @@ -134,8 +252,8 @@ public class PaymentAccountUtil { } public static boolean isCryptoCurrencyAccount(PaymentAccount paymentAccount) { - return (paymentAccount != null && paymentAccount.getPaymentMethod().equals(PaymentMethod.BLOCK_CHAINS) || - paymentAccount != null && paymentAccount.getPaymentMethod().equals(PaymentMethod.BLOCK_CHAINS_INSTANT)); + return (paymentAccount != null && paymentAccount.getPaymentMethod().equals(BLOCK_CHAINS) || + paymentAccount != null && paymentAccount.getPaymentMethod().equals(BLOCK_CHAINS_INSTANT)); } public static Optional findPaymentAccount(PaymentAccountPayload paymentAccountPayload, diff --git a/core/src/main/java/bisq/core/payment/PaymentAccounts.java b/core/src/main/java/bisq/core/payment/PaymentAccounts.java index 43606230b1..6b3fac174b 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccounts.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccounts.java @@ -25,7 +25,7 @@ import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Set; -import java.util.function.BiFunction; +import java.util.function.BiPredicate; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -38,14 +38,14 @@ class PaymentAccounts { private final Set accounts; private final AccountAgeWitnessService accountAgeWitnessService; - private final BiFunction validator; + private final BiPredicate validator; PaymentAccounts(Set accounts, AccountAgeWitnessService accountAgeWitnessService) { this(accounts, accountAgeWitnessService, PaymentAccountUtil::isPaymentAccountValidForOffer); } PaymentAccounts(Set accounts, AccountAgeWitnessService accountAgeWitnessService, - BiFunction validator) { + BiPredicate validator) { this.accounts = accounts; this.accountAgeWitnessService = accountAgeWitnessService; this.validator = validator; @@ -63,7 +63,7 @@ class PaymentAccounts { private List sortValidAccounts(Offer offer) { Comparator comparator = this::compareByTradeLimit; return accounts.stream() - .filter(account -> validator.apply(offer, account)) + .filter(account -> validator.test(offer, account)) .sorted(comparator.reversed()) .collect(Collectors.toList()); } diff --git a/core/src/main/java/bisq/core/payment/PayseraAccount.java b/core/src/main/java/bisq/core/payment/PayseraAccount.java new file mode 100644 index 0000000000..078e7d5e00 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/PayseraAccount.java @@ -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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.PayseraAccountPayload; + +import java.util.List; + +import lombok.EqualsAndHashCode; + +import org.jetbrains.annotations.NotNull; + +@EqualsAndHashCode(callSuper = true) +public final class PayseraAccount extends PaymentAccount { + + // https://github.com/bisq-network/growth/issues/233 + public static final List SUPPORTED_CURRENCIES = List.of( + new FiatCurrency("AUD"), + new FiatCurrency("BGN"), + new FiatCurrency("BYN"), + new FiatCurrency("CAD"), + new FiatCurrency("CHF"), + new FiatCurrency("CNY"), + new FiatCurrency("CZK"), + new FiatCurrency("DKK"), + new FiatCurrency("EUR"), + new FiatCurrency("GBP"), + new FiatCurrency("GEL"), + new FiatCurrency("HKD"), + new FiatCurrency("HRK"), + new FiatCurrency("HUF"), + new FiatCurrency("ILS"), + new FiatCurrency("INR"), + new FiatCurrency("JPY"), + new FiatCurrency("KZT"), + new FiatCurrency("MXN"), + new FiatCurrency("NOK"), + new FiatCurrency("NZD"), + new FiatCurrency("PHP"), + new FiatCurrency("PLN"), + new FiatCurrency("RON"), + new FiatCurrency("RSD"), + new FiatCurrency("RUB"), + new FiatCurrency("SEK"), + new FiatCurrency("SGD"), + new FiatCurrency("THB"), + new FiatCurrency("TRY"), + new FiatCurrency("USD"), + new FiatCurrency("ZAR") + ); + + public PayseraAccount() { + super(PaymentMethod.PAYSERA); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new PayseraAccountPayload(paymentMethod.getId(), id); + } + + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + + public void setEmail(String accountId) { + ((PayseraAccountPayload) paymentAccountPayload).setEmail(accountId); + } + + public String getEmail() { + return ((PayseraAccountPayload) paymentAccountPayload).getEmail(); + } +} diff --git a/core/src/main/java/bisq/core/payment/PaytmAccount.java b/core/src/main/java/bisq/core/payment/PaytmAccount.java new file mode 100644 index 0000000000..2630cf510d --- /dev/null +++ b/core/src/main/java/bisq/core/payment/PaytmAccount.java @@ -0,0 +1,56 @@ +/* + * 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 . + */ + +package bisq.core.payment; + +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.PaytmAccountPayload; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class PaytmAccount extends IfscBasedAccount { + public PaytmAccount() { + super(PaymentMethod.PAYTM); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new PaytmAccountPayload(paymentMethod.getId(), id); + } + + public void setEmailOrMobileNr(String emailOrMobileNr) { + ((PaytmAccountPayload) paymentAccountPayload).setEmailOrMobileNr(emailOrMobileNr); + } + + public String getEmailOrMobileNr() { + return ((PaytmAccountPayload) paymentAccountPayload).getEmailOrMobileNr(); + } + + public String getMessageForBuyer() { + return "payment.paytm.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.paytm.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.paytm.info.account"; + } +} diff --git a/core/src/main/java/bisq/core/payment/PerfectMoneyAccount.java b/core/src/main/java/bisq/core/payment/PerfectMoneyAccount.java index 0ed401c932..cafdfec4e1 100644 --- a/core/src/main/java/bisq/core/payment/PerfectMoneyAccount.java +++ b/core/src/main/java/bisq/core/payment/PerfectMoneyAccount.java @@ -18,17 +18,24 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.PerfectMoneyAccountPayload; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class PerfectMoneyAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("USD")); + public PerfectMoneyAccount() { super(PaymentMethod.PERFECT_MONEY); - setSingleTradeCurrency(new FiatCurrency("USD")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -36,6 +43,11 @@ public final class PerfectMoneyAccount extends PaymentAccount { return new PerfectMoneyAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setAccountNr(String accountNr) { ((PerfectMoneyAccountPayload) paymentAccountPayload).setAccountNr(accountNr); } diff --git a/core/src/main/java/bisq/core/payment/PixAccount.java b/core/src/main/java/bisq/core/payment/PixAccount.java new file mode 100644 index 0000000000..36f221c0db --- /dev/null +++ b/core/src/main/java/bisq/core/payment/PixAccount.java @@ -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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.PixAccountPayload; + +import java.util.List; + +import lombok.EqualsAndHashCode; +import lombok.NonNull; + +@EqualsAndHashCode(callSuper = true) +public final class PixAccount extends CountryBasedPaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("BRL")); + + public PixAccount() { + super(PaymentMethod.PIX); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new PixAccountPayload(paymentMethod.getId(), id); + } + + public void setPixKey(String pixKey) { + ((PixAccountPayload) paymentAccountPayload).setPixKey(pixKey); + } + + public String getPixKey() { + return ((PixAccountPayload) paymentAccountPayload).getPixKey(); + } + + public String getMessageForBuyer() { + return "payment.pix.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.pix.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.pix.info.account"; + } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/PopmoneyAccount.java b/core/src/main/java/bisq/core/payment/PopmoneyAccount.java index b535c6a273..2c9f3892d1 100644 --- a/core/src/main/java/bisq/core/payment/PopmoneyAccount.java +++ b/core/src/main/java/bisq/core/payment/PopmoneyAccount.java @@ -18,18 +18,25 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.PopmoneyAccountPayload; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; //TODO missing support for selected trade currency @EqualsAndHashCode(callSuper = true) public final class PopmoneyAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("USD")); + public PopmoneyAccount() { super(PaymentMethod.POPMONEY); - setSingleTradeCurrency(new FiatCurrency("USD")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -37,6 +44,11 @@ public final class PopmoneyAccount extends PaymentAccount { return new PopmoneyAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setAccountId(String accountId) { ((PopmoneyAccountPayload) paymentAccountPayload).setAccountId(accountId); } diff --git a/core/src/main/java/bisq/core/payment/PromptPayAccount.java b/core/src/main/java/bisq/core/payment/PromptPayAccount.java index 8675d633de..6c3eb9b435 100644 --- a/core/src/main/java/bisq/core/payment/PromptPayAccount.java +++ b/core/src/main/java/bisq/core/payment/PromptPayAccount.java @@ -18,17 +18,24 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.PromptPayAccountPayload; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class PromptPayAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("THB")); + public PromptPayAccount() { super(PaymentMethod.PROMPT_PAY); - setSingleTradeCurrency(new FiatCurrency("THB")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -36,6 +43,11 @@ public final class PromptPayAccount extends PaymentAccount { return new PromptPayAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setPromptPayId(String promptPayId) { ((PromptPayAccountPayload) paymentAccountPayload).setPromptPayId(promptPayId); } diff --git a/core/src/main/java/bisq/core/payment/RevolutAccount.java b/core/src/main/java/bisq/core/payment/RevolutAccount.java index 9494520bef..59b610abe3 100644 --- a/core/src/main/java/bisq/core/payment/RevolutAccount.java +++ b/core/src/main/java/bisq/core/payment/RevolutAccount.java @@ -17,18 +17,58 @@ package bisq.core.payment; -import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.RevolutAccountPayload; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class RevolutAccount extends PaymentAccount { + + // https://www.revolut.com/help/getting-started/exchanging-currencies/what-fiat-currencies-are-supported-for-holding-and-exchange + public static final List SUPPORTED_CURRENCIES = List.of( + new FiatCurrency("AED"), + new FiatCurrency("AUD"), + new FiatCurrency("BGN"), + new FiatCurrency("CAD"), + new FiatCurrency("CHF"), + new FiatCurrency("CZK"), + new FiatCurrency("DKK"), + new FiatCurrency("EUR"), + new FiatCurrency("GBP"), + new FiatCurrency("HKD"), + new FiatCurrency("HRK"), + new FiatCurrency("HUF"), + new FiatCurrency("ILS"), + new FiatCurrency("ISK"), + new FiatCurrency("JPY"), + new FiatCurrency("MAD"), + new FiatCurrency("MXN"), + new FiatCurrency("NOK"), + new FiatCurrency("NZD"), + new FiatCurrency("PLN"), + new FiatCurrency("QAR"), + new FiatCurrency("RON"), + new FiatCurrency("RSD"), + new FiatCurrency("RUB"), + new FiatCurrency("SAR"), + new FiatCurrency("SEK"), + new FiatCurrency("SGD"), + new FiatCurrency("THB"), + new FiatCurrency("TRY"), + new FiatCurrency("USD"), + new FiatCurrency("ZAR") + ); + public RevolutAccount() { super(PaymentMethod.REVOLUT); - tradeCurrencies.addAll(CurrencyUtil.getAllRevolutCurrencies()); + tradeCurrencies.addAll(getSupportedCurrencies()); } @Override @@ -67,4 +107,9 @@ public final class RevolutAccount extends PaymentAccount { // At save we apply the userName to accountId in case it is empty for backward compatibility revolutAccountPayload().maybeApplyUserNameToAccountId(); } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } } diff --git a/core/src/main/java/bisq/core/payment/RtgsAccount.java b/core/src/main/java/bisq/core/payment/RtgsAccount.java new file mode 100644 index 0000000000..eac2205c0c --- /dev/null +++ b/core/src/main/java/bisq/core/payment/RtgsAccount.java @@ -0,0 +1,49 @@ +/* + * 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 . + */ + +package bisq.core.payment; + +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.RtgsAccountPayload; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class RtgsAccount extends IfscBasedAccount { + + public RtgsAccount() { + super(PaymentMethod.RTGS); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new RtgsAccountPayload(paymentMethod.getId(), id); + } + + public String getMessageForBuyer() { + return "payment.rtgs.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.rtgs.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.rtgs.info.account"; + } +} diff --git a/core/src/main/java/bisq/core/payment/SameBankAccount.java b/core/src/main/java/bisq/core/payment/SameBankAccount.java index c3612b87ad..e08c683cc1 100644 --- a/core/src/main/java/bisq/core/payment/SameBankAccount.java +++ b/core/src/main/java/bisq/core/payment/SameBankAccount.java @@ -17,15 +17,23 @@ package bisq.core.payment; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.BankAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.SameBankAccountPayload; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class SameBankAccount extends CountryBasedPaymentAccount implements BankNameRestrictedBankAccount, SameCountryRestrictedBankAccount { + + public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllFiatCurrencies(); + public SameBankAccount() { super(PaymentMethod.SAME_BANK); } @@ -35,6 +43,11 @@ public final class SameBankAccount extends CountryBasedPaymentAccount implements return new SameBankAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + @Override public String getBankId() { return ((BankAccountPayload) paymentAccountPayload).getBankId(); diff --git a/core/src/main/java/bisq/core/payment/SatispayAccount.java b/core/src/main/java/bisq/core/payment/SatispayAccount.java new file mode 100644 index 0000000000..7a40bac6ae --- /dev/null +++ b/core/src/main/java/bisq/core/payment/SatispayAccount.java @@ -0,0 +1,77 @@ +/* + * 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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.SatispayAccountPayload; + +import java.util.List; + +import lombok.EqualsAndHashCode; +import lombok.NonNull; + +@EqualsAndHashCode(callSuper = true) +public final class SatispayAccount extends CountryBasedPaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("EUR")); + + public SatispayAccount() { + super(PaymentMethod.SATISPAY); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new SatispayAccountPayload(paymentMethod.getId(), id); + } + + public void setHolderName(String accountId) { + ((SatispayAccountPayload) paymentAccountPayload).setHolderName(accountId); + } + + public String getHolderName() { + return ((SatispayAccountPayload) paymentAccountPayload).getHolderName(); + } + + public void setMobileNr(String accountId) { + ((SatispayAccountPayload) paymentAccountPayload).setMobileNr(accountId); + } + + public String getMobileNr() { + return ((SatispayAccountPayload) paymentAccountPayload).getMobileNr(); + } + + public String getMessageForBuyer() { + return "payment.satispay.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.satispay.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.satispay.info.account"; + } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/SepaAccount.java b/core/src/main/java/bisq/core/payment/SepaAccount.java index 7d30e361ba..be1a93a732 100644 --- a/core/src/main/java/bisq/core/payment/SepaAccount.java +++ b/core/src/main/java/bisq/core/payment/SepaAccount.java @@ -18,6 +18,8 @@ package bisq.core.payment; import bisq.core.locale.CountryUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.SepaAccountPayload; @@ -26,10 +28,16 @@ import java.util.List; import lombok.EqualsAndHashCode; +import org.jetbrains.annotations.NotNull; + @EqualsAndHashCode(callSuper = true) public final class SepaAccount extends CountryBasedPaymentAccount implements BankAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("EUR")); + public SepaAccount() { super(PaymentMethod.SEPA); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -78,4 +86,22 @@ public final class SepaAccount extends CountryBasedPaymentAccount implements Ban public void removeAcceptedCountry(String countryCode) { ((SepaAccountPayload) paymentAccountPayload).removeAcceptedCountry(countryCode); } + + @Override + public void onPersistChanges() { + super.onPersistChanges(); + ((SepaAccountPayload) paymentAccountPayload).onPersistChanges(); + } + + @Override + public void revertChanges() { + super.revertChanges(); + ((SepaAccountPayload) paymentAccountPayload).revertChanges(); + } + + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } } diff --git a/core/src/main/java/bisq/core/payment/SepaInstantAccount.java b/core/src/main/java/bisq/core/payment/SepaInstantAccount.java index ae54b2bf8d..6ad8669e8e 100644 --- a/core/src/main/java/bisq/core/payment/SepaInstantAccount.java +++ b/core/src/main/java/bisq/core/payment/SepaInstantAccount.java @@ -18,6 +18,8 @@ package bisq.core.payment; import bisq.core.locale.CountryUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.SepaInstantAccountPayload; @@ -26,10 +28,16 @@ import java.util.List; import lombok.EqualsAndHashCode; +import org.jetbrains.annotations.NotNull; + @EqualsAndHashCode(callSuper = true) public final class SepaInstantAccount extends CountryBasedPaymentAccount implements BankAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("EUR")); + public SepaInstantAccount() { super(PaymentMethod.SEPA_INSTANT); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -78,4 +86,22 @@ public final class SepaInstantAccount extends CountryBasedPaymentAccount impleme public void removeAcceptedCountry(String countryCode) { ((SepaInstantAccountPayload) paymentAccountPayload).removeAcceptedCountry(countryCode); } + + @Override + public void onPersistChanges() { + super.onPersistChanges(); + ((SepaInstantAccountPayload) paymentAccountPayload).onPersistChanges(); + } + + @Override + public void revertChanges() { + super.revertChanges(); + ((SepaInstantAccountPayload) paymentAccountPayload).revertChanges(); + } + + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } } diff --git a/core/src/main/java/bisq/core/payment/SpecificBanksAccount.java b/core/src/main/java/bisq/core/payment/SpecificBanksAccount.java index f6f25da798..4950b98d6c 100644 --- a/core/src/main/java/bisq/core/payment/SpecificBanksAccount.java +++ b/core/src/main/java/bisq/core/payment/SpecificBanksAccount.java @@ -17,16 +17,23 @@ package bisq.core.payment; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.SpecificBanksAccountPayload; import java.util.ArrayList; +import java.util.List; import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class SpecificBanksAccount extends CountryBasedPaymentAccount implements BankNameRestrictedBankAccount, SameCountryRestrictedBankAccount { + + public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllFiatCurrencies(); + public SpecificBanksAccount() { super(PaymentMethod.SPECIFIC_BANKS); } @@ -36,6 +43,11 @@ public final class SpecificBanksAccount extends CountryBasedPaymentAccount imple return new SpecificBanksAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + // TODO change to List public ArrayList getAcceptedBanks() { return ((SpecificBanksAccountPayload) paymentAccountPayload).getAcceptedBanks(); diff --git a/core/src/main/java/bisq/core/payment/StrikeAccount.java b/core/src/main/java/bisq/core/payment/StrikeAccount.java new file mode 100644 index 0000000000..6ae4933b16 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/StrikeAccount.java @@ -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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.StrikeAccountPayload; + +import java.util.List; + +import lombok.EqualsAndHashCode; + +import org.jetbrains.annotations.NotNull; + +@EqualsAndHashCode(callSuper = true) +public final class StrikeAccount extends CountryBasedPaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("USD")); + + public StrikeAccount() { + super(PaymentMethod.STRIKE); + // this payment method is currently restricted to United States/USD + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new StrikeAccountPayload(paymentMethod.getId(), id); + } + + public void setHolderName(String accountId) { + ((StrikeAccountPayload) paymentAccountPayload).setHolderName(accountId); + } + + public String getHolderName() { + return ((StrikeAccountPayload) paymentAccountPayload).getHolderName(); + } + + public String getMessageForBuyer() { + return "payment.strike.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.strike.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.strike.info.account"; + } + + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/SwiftAccount.java b/core/src/main/java/bisq/core/payment/SwiftAccount.java new file mode 100644 index 0000000000..683ee45e43 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/SwiftAccount.java @@ -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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.SwiftAccountPayload; + +import java.util.ArrayList; +import java.util.List; + +import lombok.EqualsAndHashCode; +import lombok.NonNull; + +import static bisq.core.locale.CurrencyUtil.getAllSortedFiatCurrencies; +import static java.util.Comparator.comparing; + +@EqualsAndHashCode(callSuper = true) +public final class SwiftAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = new ArrayList<>(getAllSortedFiatCurrencies(comparing(TradeCurrency::getCode))); + + public SwiftAccount() { + super(PaymentMethod.SWIFT); + tradeCurrencies.addAll(SUPPORTED_CURRENCIES); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new SwiftAccountPayload(paymentMethod.getId(), id); + } + + public SwiftAccountPayload getPayload() { + return ((SwiftAccountPayload) this.paymentAccountPayload); + } + + public String getMessageForBuyer() { + return "payment.swift.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.swift.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.swift.info.account"; + } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/SwishAccount.java b/core/src/main/java/bisq/core/payment/SwishAccount.java index 27824c518d..0292e27e03 100644 --- a/core/src/main/java/bisq/core/payment/SwishAccount.java +++ b/core/src/main/java/bisq/core/payment/SwishAccount.java @@ -18,17 +18,24 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.SwishAccountPayload; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class SwishAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("SEK")); + public SwishAccount() { super(PaymentMethod.SWISH); - setSingleTradeCurrency(new FiatCurrency("SEK")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -36,6 +43,11 @@ public final class SwishAccount extends PaymentAccount { return new SwishAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setMobileNr(String mobileNr) { ((SwishAccountPayload) paymentAccountPayload).setMobileNr(mobileNr); } diff --git a/core/src/main/java/bisq/core/payment/TikkieAccount.java b/core/src/main/java/bisq/core/payment/TikkieAccount.java new file mode 100644 index 0000000000..21bd984a9b --- /dev/null +++ b/core/src/main/java/bisq/core/payment/TikkieAccount.java @@ -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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.TikkieAccountPayload; + +import java.util.List; + +import lombok.EqualsAndHashCode; + +import org.jetbrains.annotations.NotNull; + +@EqualsAndHashCode(callSuper = true) +public final class TikkieAccount extends CountryBasedPaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("EUR")); + + public TikkieAccount() { + super(PaymentMethod.TIKKIE); + // this payment method is only for Netherlands/EUR + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new TikkieAccountPayload(paymentMethod.getId(), id); + } + + public void setIban(String iban) { + ((TikkieAccountPayload) paymentAccountPayload).setIban(iban); + } + + public String getIban() { + return ((TikkieAccountPayload) paymentAccountPayload).getIban(); + } + + public String getMessageForBuyer() { + return "payment.tikkie.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.tikkie.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.tikkie.info.account"; + } + + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/TradeLimits.java b/core/src/main/java/bisq/core/payment/TradeLimits.java index baed68b229..33aaca22ad 100644 --- a/core/src/main/java/bisq/core/payment/TradeLimits.java +++ b/core/src/main/java/bisq/core/payment/TradeLimits.java @@ -39,7 +39,6 @@ public class TradeLimits { @Getter private static TradeLimits INSTANCE; - @Inject public TradeLimits() { INSTANCE = this; diff --git a/core/src/main/java/bisq/core/payment/TransferwiseAccount.java b/core/src/main/java/bisq/core/payment/TransferwiseAccount.java index c0363a63b5..ad5efdc6a7 100644 --- a/core/src/main/java/bisq/core/payment/TransferwiseAccount.java +++ b/core/src/main/java/bisq/core/payment/TransferwiseAccount.java @@ -17,14 +17,67 @@ package bisq.core.payment; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.TransferwiseAccountPayload; +import java.util.List; + import lombok.EqualsAndHashCode; +import org.jetbrains.annotations.NotNull; + @EqualsAndHashCode(callSuper = true) public final class TransferwiseAccount extends PaymentAccount { + + // https://github.com/bisq-network/proposals/issues/243 + public static final List SUPPORTED_CURRENCIES = List.of( + new FiatCurrency("AED"), + new FiatCurrency("ARS"), + new FiatCurrency("AUD"), + new FiatCurrency("BGN"), + new FiatCurrency("CAD"), + new FiatCurrency("CHF"), + new FiatCurrency("CLP"), + new FiatCurrency("CZK"), + new FiatCurrency("DKK"), + new FiatCurrency("EGP"), + new FiatCurrency("EUR"), + new FiatCurrency("GBP"), + new FiatCurrency("GEL"), + new FiatCurrency("HKD"), + new FiatCurrency("HRK"), + new FiatCurrency("HUF"), + new FiatCurrency("IDR"), + new FiatCurrency("ILS"), + new FiatCurrency("JPY"), + new FiatCurrency("KES"), + new FiatCurrency("KRW"), + new FiatCurrency("MAD"), + new FiatCurrency("MXN"), + new FiatCurrency("MYR"), + new FiatCurrency("NOK"), + new FiatCurrency("NPR"), + new FiatCurrency("NZD"), + new FiatCurrency("PEN"), + new FiatCurrency("PHP"), + new FiatCurrency("PKR"), + new FiatCurrency("PLN"), + new FiatCurrency("RON"), + new FiatCurrency("RUB"), + new FiatCurrency("SEK"), + new FiatCurrency("SGD"), + new FiatCurrency("THB"), + new FiatCurrency("TRY"), + new FiatCurrency("UGX"), + new FiatCurrency("VND"), + new FiatCurrency("XOF"), + new FiatCurrency("ZAR"), + new FiatCurrency("ZMW") + ); + public TransferwiseAccount() { super(PaymentMethod.TRANSFERWISE); } @@ -34,6 +87,12 @@ public final class TransferwiseAccount extends PaymentAccount { return new TransferwiseAccountPayload(paymentMethod.getId(), id); } + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setEmail(String accountId) { ((TransferwiseAccountPayload) paymentAccountPayload).setEmail(accountId); } diff --git a/core/src/main/java/bisq/core/payment/TransferwiseUsdAccount.java b/core/src/main/java/bisq/core/payment/TransferwiseUsdAccount.java new file mode 100644 index 0000000000..b27d760c2e --- /dev/null +++ b/core/src/main/java/bisq/core/payment/TransferwiseUsdAccount.java @@ -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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.TransferwiseUsdAccountPayload; + +import java.util.List; + +import lombok.EqualsAndHashCode; + +import org.jetbrains.annotations.NotNull; + +@EqualsAndHashCode(callSuper = true) +public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("USD")); + + public TransferwiseUsdAccount() { + super(PaymentMethod.TRANSFERWISE_USD); + // this payment method is currently restricted to United States/USD + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new TransferwiseUsdAccountPayload(paymentMethod.getId(), id); + } + + public void setEmail(String email) { + ((TransferwiseUsdAccountPayload) paymentAccountPayload).setEmail(email); + } + + public String getEmail() { + return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getEmail(); + } + + public void setHolderName(String accountId) { + ((TransferwiseUsdAccountPayload) paymentAccountPayload).setHolderName(accountId); + } + + public String getHolderName() { + return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getHolderName(); + } + + public void setBeneficiaryAddress(String address) { + ((TransferwiseUsdAccountPayload) paymentAccountPayload).setBeneficiaryAddress(address); + } + + public String getBeneficiaryAddress() { + return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getBeneficiaryAddress(); + } + + public String getMessageForBuyer() { + return "payment.transferwiseUsd.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.transferwiseUsd.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.transferwiseUsd.info.account"; + } + + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/USPostalMoneyOrderAccount.java b/core/src/main/java/bisq/core/payment/USPostalMoneyOrderAccount.java index 6360533932..443a468760 100644 --- a/core/src/main/java/bisq/core/payment/USPostalMoneyOrderAccount.java +++ b/core/src/main/java/bisq/core/payment/USPostalMoneyOrderAccount.java @@ -18,17 +18,24 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.USPostalMoneyOrderAccountPayload; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class USPostalMoneyOrderAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("USD")); + public USPostalMoneyOrderAccount() { super(PaymentMethod.US_POSTAL_MONEY_ORDER); - setSingleTradeCurrency(new FiatCurrency("USD")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -36,6 +43,11 @@ public final class USPostalMoneyOrderAccount extends PaymentAccount { return new USPostalMoneyOrderAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setPostalAddress(String postalAddress) { ((USPostalMoneyOrderAccountPayload) paymentAccountPayload).setPostalAddress(postalAddress); } diff --git a/core/src/main/java/bisq/core/payment/UpholdAccount.java b/core/src/main/java/bisq/core/payment/UpholdAccount.java index e3e84e1d91..a64441e90f 100644 --- a/core/src/main/java/bisq/core/payment/UpholdAccount.java +++ b/core/src/main/java/bisq/core/payment/UpholdAccount.java @@ -17,19 +17,52 @@ package bisq.core.payment; -import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.UpholdAccountPayload; +import java.util.List; + import lombok.EqualsAndHashCode; +import org.jetbrains.annotations.NotNull; + //TODO missing support for selected trade currency @EqualsAndHashCode(callSuper = true) public final class UpholdAccount extends PaymentAccount { + + // https://support.uphold.com/hc/en-us/articles/202473803-Supported-currencies + public static final List SUPPORTED_CURRENCIES = List.of( + new FiatCurrency("AED"), + new FiatCurrency("ARS"), + new FiatCurrency("AUD"), + new FiatCurrency("BRL"), + new FiatCurrency("CAD"), + new FiatCurrency("CHF"), + new FiatCurrency("CNY"), + new FiatCurrency("DKK"), + new FiatCurrency("EUR"), + new FiatCurrency("GBP"), + new FiatCurrency("HKD"), + new FiatCurrency("ILS"), + new FiatCurrency("INR"), + new FiatCurrency("JPY"), + new FiatCurrency("KES"), + new FiatCurrency("MXN"), + new FiatCurrency("NOK"), + new FiatCurrency("NZD"), + new FiatCurrency("PHP"), + new FiatCurrency("PLN"), + new FiatCurrency("SEK"), + new FiatCurrency("SGD"), + new FiatCurrency("USD") + ); + public UpholdAccount() { super(PaymentMethod.UPHOLD); - tradeCurrencies.addAll(CurrencyUtil.getAllUpholdCurrencies()); + tradeCurrencies.addAll(SUPPORTED_CURRENCIES); } @Override @@ -37,6 +70,12 @@ public final class UpholdAccount extends PaymentAccount { return new UpholdAccountPayload(paymentMethod.getId(), id); } + @NotNull + @Override + public List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setAccountId(String accountId) { ((UpholdAccountPayload) paymentAccountPayload).setAccountId(accountId); } @@ -44,4 +83,15 @@ public final class UpholdAccount extends PaymentAccount { public String getAccountId() { return ((UpholdAccountPayload) paymentAccountPayload).getAccountId(); } + + public String getAccountOwner() { + return ((UpholdAccountPayload) paymentAccountPayload).getAccountOwner(); + } + + public void setAccountOwner(String accountOwner) { + if (accountOwner == null) { + accountOwner = ""; + } + ((UpholdAccountPayload) paymentAccountPayload).setAccountOwner(accountOwner); + } } diff --git a/core/src/main/java/bisq/core/payment/UpiAccount.java b/core/src/main/java/bisq/core/payment/UpiAccount.java new file mode 100644 index 0000000000..26197498b3 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/UpiAccount.java @@ -0,0 +1,56 @@ +/* + * 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 . + */ + +package bisq.core.payment; + +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.UpiAccountPayload; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class UpiAccount extends IfscBasedAccount { + public UpiAccount() { + super(PaymentMethod.UPI); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new UpiAccountPayload(paymentMethod.getId(), id); + } + + public void setVirtualPaymentAddress(String virtualPaymentAddress) { + ((UpiAccountPayload) paymentAccountPayload).setVirtualPaymentAddress(virtualPaymentAddress); + } + + public String getVirtualPaymentAddress() { + return ((UpiAccountPayload) paymentAccountPayload).getVirtualPaymentAddress(); + } + + public String getMessageForBuyer() { + return "payment.upi.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.upi.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.upi.info.account"; + } +} diff --git a/core/src/main/java/bisq/core/payment/VenmoAccount.java b/core/src/main/java/bisq/core/payment/VenmoAccount.java index 7581b573b2..568bab0221 100644 --- a/core/src/main/java/bisq/core/payment/VenmoAccount.java +++ b/core/src/main/java/bisq/core/payment/VenmoAccount.java @@ -18,20 +18,27 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.VenmoAccountPayload; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; // Removed due too high chargeback risk // Cannot be deleted as it would break old trade history entries @Deprecated @EqualsAndHashCode(callSuper = true) public final class VenmoAccount extends PaymentAccount { + + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("USD")); + public VenmoAccount() { super(PaymentMethod.VENMO); - setSingleTradeCurrency(new FiatCurrency("USD")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -39,6 +46,11 @@ public final class VenmoAccount extends PaymentAccount { return new VenmoAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setVenmoUserName(String venmoUserName) { ((VenmoAccountPayload) paymentAccountPayload).setVenmoUserName(venmoUserName); } diff --git a/core/src/main/java/bisq/core/payment/VerseAccount.java b/core/src/main/java/bisq/core/payment/VerseAccount.java new file mode 100644 index 0000000000..63bd53afc1 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/VerseAccount.java @@ -0,0 +1,76 @@ +/* + * 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 . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.VerseAccountPayload; + +import java.util.List; + +import lombok.EqualsAndHashCode; +import lombok.NonNull; + +@EqualsAndHashCode(callSuper = true) +public final class VerseAccount extends PaymentAccount { + + // https://github.com/bisq-network/growth/issues/223 + public static final List SUPPORTED_CURRENCIES = List.of( + new FiatCurrency("DKK"), + new FiatCurrency("EUR"), + new FiatCurrency("HUF"), + new FiatCurrency("PLN"), + new FiatCurrency("SEK") + ); + + public VerseAccount() { + super(PaymentMethod.VERSE); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new VerseAccountPayload(paymentMethod.getId(), id); + } + + public void setHolderName(String accountId) { + ((VerseAccountPayload) paymentAccountPayload).setHolderName(accountId); + } + + public String getHolderName() { + return ((VerseAccountPayload) paymentAccountPayload).getHolderName(); + } + + public String getMessageForBuyer() { + return "payment.verse.info.buyer"; + } + + public String getMessageForSeller() { + return "payment.verse.info.seller"; + } + + public String getMessageForAccountCreation() { + return "payment.verse.info.account"; + } + + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } +} diff --git a/core/src/main/java/bisq/core/payment/WeChatPayAccount.java b/core/src/main/java/bisq/core/payment/WeChatPayAccount.java index eb1813e808..8b76b4ca38 100644 --- a/core/src/main/java/bisq/core/payment/WeChatPayAccount.java +++ b/core/src/main/java/bisq/core/payment/WeChatPayAccount.java @@ -18,18 +18,24 @@ package bisq.core.payment; import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.WeChatPayAccountPayload; +import java.util.List; + import lombok.EqualsAndHashCode; +import lombok.NonNull; @EqualsAndHashCode(callSuper = true) public final class WeChatPayAccount extends PaymentAccount { + public static final List SUPPORTED_CURRENCIES = List.of(new FiatCurrency("CNY")); + public WeChatPayAccount() { super(PaymentMethod.WECHAT_PAY); - setSingleTradeCurrency(new FiatCurrency("CNY")); + setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override @@ -37,6 +43,11 @@ public final class WeChatPayAccount extends PaymentAccount { return new WeChatPayAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public void setAccountNr(String accountNr) { ((WeChatPayAccountPayload) paymentAccountPayload).setAccountNr(accountNr); } diff --git a/core/src/main/java/bisq/core/payment/WesternUnionAccount.java b/core/src/main/java/bisq/core/payment/WesternUnionAccount.java index cd1146aafa..a268af2786 100644 --- a/core/src/main/java/bisq/core/payment/WesternUnionAccount.java +++ b/core/src/main/java/bisq/core/payment/WesternUnionAccount.java @@ -17,11 +17,20 @@ package bisq.core.payment; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.TradeCurrency; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.WesternUnionAccountPayload; +import java.util.List; + +import lombok.NonNull; + public final class WesternUnionAccount extends CountryBasedPaymentAccount { + + public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllFiatCurrencies(); + public WesternUnionAccount() { super(PaymentMethod.WESTERN_UNION); } @@ -31,6 +40,11 @@ public final class WesternUnionAccount extends CountryBasedPaymentAccount { return new WesternUnionAccountPayload(paymentMethod.getId(), id); } + @Override + public @NonNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + public String getEmail() { return ((WesternUnionAccountPayload) paymentAccountPayload).getEmail(); } diff --git a/core/src/main/java/bisq/core/payment/payload/AchTransferAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/AchTransferAccountPayload.java new file mode 100644 index 0000000000..0e7b697097 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/AchTransferAccountPayload.java @@ -0,0 +1,115 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Getter +@Setter +@Slf4j +public final class AchTransferAccountPayload extends BankAccountPayload { + private String holderAddress = ""; + + public AchTransferAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private AchTransferAccountPayload(String paymentMethodName, + String id, + String countryCode, + String holderName, + String bankName, + String branchId, + String accountNr, + String accountType, + String holderAddress, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + countryCode, + holderName, + bankName, + branchId, + accountNr, + accountType, + null, // holderTaxId not used + null, // bankId not used + null, // nationalAccountId not used + maxTradePeriod, + excludeFromJsonDataMap); + + this.holderAddress = holderAddress; + } + + @Override + public Message toProtoMessage() { + protobuf.AchTransferAccountPayload.Builder builder = protobuf.AchTransferAccountPayload.newBuilder() + .setHolderAddress(holderAddress); + protobuf.BankAccountPayload.Builder bankAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .getBankAccountPayloadBuilder() + .setAchTransferAccountPayload(builder); + protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setBankAccountPayload(bankAccountPayloadBuilder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) + .build(); + } + + public static AchTransferAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.BankAccountPayload bankAccountPayloadPB = countryBasedPaymentAccountPayload.getBankAccountPayload(); + protobuf.AchTransferAccountPayload accountPayloadPB = bankAccountPayloadPB.getAchTransferAccountPayload(); + return new AchTransferAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + bankAccountPayloadPB.getHolderName(), + bankAccountPayloadPB.getBankName().isEmpty() ? null : bankAccountPayloadPB.getBankName(), + bankAccountPayloadPB.getBranchId().isEmpty() ? null : bankAccountPayloadPB.getBranchId(), + bankAccountPayloadPB.getAccountNr().isEmpty() ? null : bankAccountPayloadPB.getAccountNr(), + bankAccountPayloadPB.getAccountType().isEmpty() ? null : bankAccountPayloadPB.getAccountType(), + accountPayloadPB.getHolderAddress().isEmpty() ? null : accountPayloadPB.getHolderAddress(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/AustraliaPayidPayload.java b/core/src/main/java/bisq/core/payment/payload/AustraliaPayidAccountPayload.java similarity index 82% rename from core/src/main/java/bisq/core/payment/payload/AustraliaPayidPayload.java rename to core/src/main/java/bisq/core/payment/payload/AustraliaPayidAccountPayload.java index 077304e124..046ca48b08 100644 --- a/core/src/main/java/bisq/core/payment/payload/AustraliaPayidPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/AustraliaPayidAccountPayload.java @@ -39,11 +39,11 @@ import lombok.extern.slf4j.Slf4j; @Setter @Getter @Slf4j -public final class AustraliaPayidPayload extends PaymentAccountPayload { +public final class AustraliaPayidAccountPayload extends PaymentAccountPayload { private String payid = ""; private String bankAccountName = ""; - public AustraliaPayidPayload(String paymentMethod, String id) { + public AustraliaPayidAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } @@ -52,12 +52,12 @@ public final class AustraliaPayidPayload extends PaymentAccountPayload { // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private AustraliaPayidPayload(String paymentMethod, - String id, - String payid, - String bankAccountName, - long maxTradePeriod, - Map excludeFromJsonDataMap) { + private AustraliaPayidAccountPayload(String paymentMethod, + String id, + String payid, + String bankAccountName, + long maxTradePeriod, + Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, @@ -77,9 +77,9 @@ public final class AustraliaPayidPayload extends PaymentAccountPayload { ).build(); } - public static AustraliaPayidPayload fromProto(protobuf.PaymentAccountPayload proto) { + public static AustraliaPayidAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.AustraliaPayidPayload AustraliaPayidPayload = proto.getAustraliaPayidPayload(); - return new AustraliaPayidPayload(proto.getPaymentMethodId(), + return new AustraliaPayidAccountPayload(proto.getPaymentMethodId(), proto.getId(), AustraliaPayidPayload.getPayid(), AustraliaPayidPayload.getBankAccountName(), diff --git a/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java index a3fac6cfd1..ed398684d8 100644 --- a/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java @@ -41,18 +41,14 @@ import javax.annotation.Nullable; @Slf4j public abstract class BankAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { protected String holderName = ""; - @Nullable - protected String bankName; - @Nullable - protected String branchId; - @Nullable - protected String accountNr; + protected String bankName = ""; + protected String branchId = ""; + protected String accountNr = ""; @Nullable protected String accountType; @Nullable protected String holderTaxId; - @Nullable - protected String bankId; + protected String bankId = ""; @Nullable protected String nationalAccountId; diff --git a/core/src/main/java/bisq/core/payment/payload/BizumAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/BizumAccountPayload.java new file mode 100644 index 0000000000..15f5d5230e --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/BizumAccountPayload.java @@ -0,0 +1,99 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class BizumAccountPayload extends CountryBasedPaymentAccountPayload { + private String mobileNr = ""; + + public BizumAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private BizumAccountPayload(String paymentMethod, + String id, + String countryCode, + String mobileNr, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + + this.mobileNr = mobileNr; + } + + @Override + public Message toProtoMessage() { + protobuf.BizumAccountPayload.Builder builder = protobuf.BizumAccountPayload.newBuilder() + .setMobileNr(mobileNr); + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setBizumAccountPayload(builder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) + .build(); + } + + public static BizumAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.BizumAccountPayload paytmAccountPayloadPB = countryBasedPaymentAccountPayload.getBizumAccountPayload(); + return new BizumAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + paytmAccountPayloadPB.getMobileNr(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.mobile") + " " + mobileNr; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(mobileNr.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/CapitualAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/CapitualAccountPayload.java new file mode 100644 index 0000000000..a765d7cdeb --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/CapitualAccountPayload.java @@ -0,0 +1,99 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class CapitualAccountPayload extends PaymentAccountPayload { + private String accountNr = ""; + + public CapitualAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private CapitualAccountPayload(String paymentMethod, + String id, + String accountNr, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.accountNr = accountNr; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setCapitualAccountPayload(protobuf.CapitualAccountPayload.newBuilder() + .setAccountNr(accountNr)) + .build(); + } + + public static CapitualAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new CapitualAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getCapitualAccountPayload().getAccountNr(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.capitual.cap") + " " + accountNr; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java index 945cf12aa8..f06b429031 100644 --- a/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java @@ -23,8 +23,6 @@ import bisq.core.locale.Res; import com.google.protobuf.Message; -import java.nio.charset.StandardCharsets; - import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -42,26 +40,11 @@ import javax.annotation.Nullable; @Setter @Getter @Slf4j -public class CashDepositAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { - private String holderName = ""; +public class CashDepositAccountPayload extends BankAccountPayload { @Nullable private String holderEmail; @Nullable - private String bankName; - @Nullable - private String branchId; - @Nullable - private String accountNr; - @Nullable - private String accountType; - @Nullable private String requirements; - @Nullable - private String holderTaxId; - @Nullable - private String bankId; - @Nullable - protected String nationalAccountId; public CashDepositAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); @@ -90,18 +73,19 @@ public class CashDepositAccountPayload extends CountryBasedPaymentAccountPayload super(paymentMethodName, id, countryCode, + holderName, + bankName, + branchId, + accountNr, + accountType, + holderTaxId, + bankId, + nationalAccountId, maxTradePeriod, excludeFromJsonDataMap); - this.holderName = holderName; + this.holderEmail = holderEmail; - this.bankName = bankName; - this.branchId = branchId; - this.accountNr = accountNr; - this.accountType = accountType; this.requirements = requirements; - this.holderTaxId = holderTaxId; - this.bankId = bankId; - this.nationalAccountId = nationalAccountId; } @Override @@ -190,43 +174,4 @@ public class CashDepositAccountPayload extends CountryBasedPaymentAccountPayload requirementsString + Res.getWithCol("payment.bank.country") + " " + CountryUtil.getNameByCode(countryCode); } - - public String getHolderIdLabel() { - return BankUtil.getHolderIdLabel(countryCode); - } - - @Nullable - public String getBankId() { - return BankUtil.isBankIdRequired(countryCode) ? bankId : bankName; - } - - @Override - public byte[] getAgeWitnessInputData() { - String bankName = BankUtil.isBankNameRequired(countryCode) ? this.bankName : ""; - String bankId = BankUtil.isBankIdRequired(countryCode) ? this.bankId : ""; - String branchId = BankUtil.isBranchIdRequired(countryCode) ? this.branchId : ""; - String accountNr = BankUtil.isAccountNrRequired(countryCode) ? this.accountNr : ""; - String accountType = BankUtil.isAccountTypeRequired(countryCode) ? this.accountType : ""; - String holderTaxIdString = BankUtil.isHolderIdRequired(countryCode) ? - (BankUtil.getHolderIdLabel(countryCode) + " " + holderTaxId + "\n") : ""; - String nationalAccountId = BankUtil.isNationalAccountIdRequired(countryCode) ? this.nationalAccountId : ""; - - // We don't add holderName and holderEmail because we don't want to break age validation if the user recreates an account with - // slight changes in holder name (e.g. add or remove middle name) - - String all = bankName + - bankId + - branchId + - accountNr + - accountType + - holderTaxIdString + - nationalAccountId; - - return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); - } - - @Override - public String getOwnerId() { - return holderName; - } } diff --git a/core/src/main/java/bisq/core/payment/payload/CelPayAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/CelPayAccountPayload.java new file mode 100644 index 0000000000..2c9a62a53e --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/CelPayAccountPayload.java @@ -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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class CelPayAccountPayload extends PaymentAccountPayload { + private String email = ""; + + public CelPayAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private CelPayAccountPayload(String paymentMethod, + String id, + String email, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.email = email; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setCelPayAccountPayload(protobuf.CelPayAccountPayload.newBuilder().setEmail(email)) + .build(); + } + + public static CelPayAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new CelPayAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getCelPayAccountPayload().getEmail(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email") + " " + email; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(email.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java index e829c8e760..6446e3d956 100644 --- a/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java @@ -32,6 +32,9 @@ import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; +// Removed due to QuickPay becoming Zelle +// Cannot be deleted as it would break old trade history entries +@Deprecated @EqualsAndHashCode(callSuper = true) @ToString @Setter diff --git a/core/src/main/java/bisq/core/payment/payload/DomesticWireTransferAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/DomesticWireTransferAccountPayload.java new file mode 100644 index 0000000000..5d1fb76fda --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/DomesticWireTransferAccountPayload.java @@ -0,0 +1,119 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.BankUtil; +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Getter +@Setter +@Slf4j +public final class DomesticWireTransferAccountPayload extends BankAccountPayload { + private String holderAddress = ""; + + public DomesticWireTransferAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private DomesticWireTransferAccountPayload(String paymentMethodName, + String id, + String countryCode, + String holderName, + String bankName, + String branchId, + String accountNr, + String holderAddress, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + countryCode, + holderName, + bankName, + branchId, + accountNr, + null, + null, // holderTaxId not used + null, // bankId not used + null, // nationalAccountId not used + maxTradePeriod, + excludeFromJsonDataMap); + + this.holderAddress = holderAddress; + } + + @Override + public Message toProtoMessage() { + protobuf.DomesticWireTransferAccountPayload.Builder builder = protobuf.DomesticWireTransferAccountPayload.newBuilder() + .setHolderAddress(holderAddress); + protobuf.BankAccountPayload.Builder bankAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .getBankAccountPayloadBuilder() + .setDomesticWireTransferAccountPayload(builder); + protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setBankAccountPayload(bankAccountPayloadBuilder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) + .build(); + } + + public static DomesticWireTransferAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.BankAccountPayload bankAccountPayloadPB = countryBasedPaymentAccountPayload.getBankAccountPayload(); + protobuf.DomesticWireTransferAccountPayload accountPayloadPB = bankAccountPayloadPB.getDomesticWireTransferAccountPayload(); + return new DomesticWireTransferAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + bankAccountPayloadPB.getHolderName(), + bankAccountPayloadPB.getBankName().isEmpty() ? null : bankAccountPayloadPB.getBankName(), + bankAccountPayloadPB.getBranchId().isEmpty() ? null : bankAccountPayloadPB.getBranchId(), + bankAccountPayloadPB.getAccountNr().isEmpty() ? null : bankAccountPayloadPB.getAccountNr(), + accountPayloadPB.getHolderAddress().isEmpty() ? null : accountPayloadPB.getHolderAddress(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + String paymentDetails = (Res.get(paymentMethodId) + " - " + + Res.getWithCol("payment.account.owner") + " " + holderName + ", " + + BankUtil.getBankNameLabel(countryCode) + ": " + this.bankName + ", " + + BankUtil.getBranchIdLabel(countryCode) + ": " + this.branchId + ", " + + BankUtil.getAccountNrLabel(countryCode) + ": " + this.accountNr); + return paymentDetails; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/IfscBasedAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/IfscBasedAccountPayload.java new file mode 100644 index 0000000000..12945c335f --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/IfscBasedAccountPayload.java @@ -0,0 +1,96 @@ +package bisq.core.payment.payload; + + +import bisq.core.locale.BankUtil; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; + +import java.nio.charset.StandardCharsets; + +import java.util.Map; +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@Setter +@Getter +@ToString +@Slf4j +public abstract class IfscBasedAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { + protected String holderName = ""; + protected String ifsc = ""; + protected String accountNr = ""; + + protected IfscBasedAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + protected IfscBasedAccountPayload(String paymentMethodName, + String id, + String countryCode, + String holderName, + String accountNr, + String ifsc, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethodName, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + + this.holderName = holderName; + this.accountNr = accountNr; + this.ifsc = ifsc; + } + + @Override + public protobuf.PaymentAccountPayload.Builder getPaymentAccountPayloadBuilder() { + protobuf.IfscBasedAccountPayload.Builder builder = + protobuf.IfscBasedAccountPayload.newBuilder() + .setHolderName(holderName); + Optional.ofNullable(ifsc).ifPresent(builder::setIfsc); + Optional.ofNullable(accountNr).ifPresent(builder::setAccountNr); + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = + super.getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setIfscBasedAccountPayload(builder); + return super.getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder); + } + + @Override + public String getPaymentDetails() { + return "Ifsc account transfer - " + getPaymentDetailsForTradePopup().replace("\n", ", "); + } + + @Override + public String getPaymentDetailsForTradePopup() { + return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" + + BankUtil.getAccountNrLabel(countryCode) + ": " + accountNr + "\n" + + BankUtil.getBankIdLabel(countryCode) + ": " + ifsc + "\n" + + Res.getWithCol("payment.bank.country") + " " + CountryUtil.getNameByCode(countryCode); + } + + @Override + public byte[] getAgeWitnessInputData() { + // We don't add holderName because we don't want to break age validation if the user recreates an account with + // slight changes in holder name (e.g. add or remove middle name) + String all = accountNr + ifsc; + return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getOwnerId() { + return holderName; + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/ImpsAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/ImpsAccountPayload.java new file mode 100644 index 0000000000..17d5780d93 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/ImpsAccountPayload.java @@ -0,0 +1,115 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class ImpsAccountPayload extends IfscBasedAccountPayload { + + public ImpsAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private ImpsAccountPayload(String paymentMethod, + String id, + String countryCode, + String holderName, + String accountNr, + String ifsc, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + countryCode, + holderName, + accountNr, + ifsc, + maxTradePeriod, + excludeFromJsonDataMap); + } + + @Override + public Message toProtoMessage() { + protobuf.IfscBasedAccountPayload.Builder ifscBasedAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .getIfscBasedAccountPayloadBuilder() + .setImpsAccountPayload(protobuf.ImpsAccountPayload.newBuilder()); + + protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setIfscBasedAccountPayload(ifscBasedAccountPayloadBuilder); + + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) + .build(); + } + + public static ImpsAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.IfscBasedAccountPayload ifscBasedAccountPayloadPB = countryBasedPaymentAccountPayload.getIfscBasedAccountPayload(); + return new ImpsAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + ifscBasedAccountPayloadPB.getHolderName(), + ifscBasedAccountPayloadPB.getAccountNr(), + ifscBasedAccountPayloadPB.getIfsc(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " + + Res.getWithCol("payment.account.no") + " " + accountNr + + Res.getWithCol("payment.ifsc") + " " + ifsc; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + String accountNr = this.accountNr == null ? "" : this.accountNr; + return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getHolderName() { + return getOwnerId(); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/MoneseAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/MoneseAccountPayload.java new file mode 100644 index 0000000000..bbc299a100 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/MoneseAccountPayload.java @@ -0,0 +1,95 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class MoneseAccountPayload extends PaymentAccountPayload { + private String holderName = ""; + private String mobileNr = ""; + + public MoneseAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private MoneseAccountPayload(String paymentMethod, + String id, + String holderName, + String mobileNr, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.holderName = holderName; + this.mobileNr = mobileNr; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setMoneseAccountPayload(protobuf.MoneseAccountPayload.newBuilder() + .setHolderName(holderName) + .setMobileNr(mobileNr)) + .build(); + } + + public static MoneseAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new MoneseAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getMoneseAccountPayload().getHolderName(), + proto.getMoneseAccountPayload().getMobileNr(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.userName") + " " + holderName; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(holderName.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/NeftAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/NeftAccountPayload.java new file mode 100644 index 0000000000..fa173eb295 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/NeftAccountPayload.java @@ -0,0 +1,115 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class NeftAccountPayload extends IfscBasedAccountPayload { + + public NeftAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private NeftAccountPayload(String paymentMethod, + String id, + String countryCode, + String holderName, + String accountNr, + String ifsc, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + countryCode, + holderName, + accountNr, + ifsc, + maxTradePeriod, + excludeFromJsonDataMap); + } + + @Override + public Message toProtoMessage() { + protobuf.IfscBasedAccountPayload.Builder ifscBasedAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .getIfscBasedAccountPayloadBuilder() + .setNeftAccountPayload(protobuf.NeftAccountPayload.newBuilder()); + + protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setIfscBasedAccountPayload(ifscBasedAccountPayloadBuilder); + + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) + .build(); + } + + public static NeftAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.IfscBasedAccountPayload ifscBasedAccountPayloadPB = countryBasedPaymentAccountPayload.getIfscBasedAccountPayload(); + return new NeftAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + ifscBasedAccountPayloadPB.getHolderName(), + ifscBasedAccountPayloadPB.getAccountNr(), + ifscBasedAccountPayloadPB.getIfsc(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " + + Res.getWithCol("payment.account.no") + " " + accountNr + + Res.getWithCol("payment.ifsc") + " " + ifsc; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + String accountNr = this.accountNr == null ? "" : this.accountNr; + return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getHolderName() { + return getOwnerId(); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/NequiAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/NequiAccountPayload.java new file mode 100644 index 0000000000..6fca432521 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/NequiAccountPayload.java @@ -0,0 +1,99 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class NequiAccountPayload extends CountryBasedPaymentAccountPayload { + private String mobileNr = ""; + + public NequiAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private NequiAccountPayload(String paymentMethod, + String id, + String countryCode, + String mobileNr, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + + this.mobileNr = mobileNr; + } + + @Override + public Message toProtoMessage() { + protobuf.NequiAccountPayload.Builder builder = protobuf.NequiAccountPayload.newBuilder() + .setMobileNr(mobileNr); + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setNequiAccountPayload(builder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) + .build(); + } + + public static NequiAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.NequiAccountPayload paytmAccountPayloadPB = countryBasedPaymentAccountPayload.getNequiAccountPayload(); + return new NequiAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + paytmAccountPayloadPB.getMobileNr(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.mobile") + " " + mobileNr; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(mobileNr.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/PaxumAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/PaxumAccountPayload.java new file mode 100644 index 0000000000..00e3c53391 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/PaxumAccountPayload.java @@ -0,0 +1,99 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class PaxumAccountPayload extends PaymentAccountPayload { + private String email = ""; + + public PaxumAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private PaxumAccountPayload(String paymentMethod, + String id, + String email, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.email = email; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setPaxumAccountPayload(protobuf.PaxumAccountPayload.newBuilder().setEmail(email)) + .build(); + } + + public static PaxumAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new PaxumAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getPaxumAccountPayload().getEmail(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email") + " " + email; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(email.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/PaymentAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/PaymentAccountPayload.java index 88af9f137f..8da65120c5 100644 --- a/core/src/main/java/bisq/core/payment/payload/PaymentAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/PaymentAccountPayload.java @@ -47,7 +47,7 @@ import static com.google.common.base.Preconditions.checkArgument; @ToString @Slf4j public abstract class PaymentAccountPayload implements NetworkPayload, UsedForTradeContractJson { - + // Keys for excludeFromJsonDataMap public static final String SALT = "salt"; public static final String HOLDER_NAME = "holderName"; @@ -118,7 +118,7 @@ public abstract class PaymentAccountPayload implements NetworkPayload, UsedForTr public abstract String getPaymentDetails(); public abstract String getPaymentDetailsForTradePopup(); - + public byte[] getHash() { return Hash.getRipemd160hash(this.toProtoMessage().toByteArray()); } diff --git a/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java b/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java index 7e7843e524..2ec8f7140b 100644 --- a/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java @@ -17,10 +17,58 @@ package bisq.core.payment.payload; +import bisq.core.payment.AchTransferAccount; +import bisq.core.payment.AdvancedCashAccount; +import bisq.core.payment.AliPayAccount; +import bisq.core.payment.AmazonGiftCardAccount; +import bisq.core.payment.AustraliaPayidAccount; +import bisq.core.payment.BizumAccount; +import bisq.core.payment.CapitualAccount; +import bisq.core.payment.CashByMailAccount; +import bisq.core.payment.CashDepositAccount; +import bisq.core.payment.CelPayAccount; +import bisq.core.payment.ClearXchangeAccount; +import bisq.core.payment.DomesticWireTransferAccount; +import bisq.core.payment.F2FAccount; +import bisq.core.payment.FasterPaymentsAccount; +import bisq.core.payment.HalCashAccount; +import bisq.core.payment.ImpsAccount; +import bisq.core.payment.InteracETransferAccount; +import bisq.core.payment.JapanBankAccount; +import bisq.core.payment.MoneseAccount; +import bisq.core.payment.MoneyBeamAccount; +import bisq.core.payment.MoneyGramAccount; +import bisq.core.payment.NationalBankAccount; +import bisq.core.payment.NeftAccount; +import bisq.core.payment.NequiAccount; +import bisq.core.payment.PaxumAccount; +import bisq.core.payment.PayseraAccount; +import bisq.core.payment.PaytmAccount; +import bisq.core.payment.PerfectMoneyAccount; +import bisq.core.payment.PixAccount; +import bisq.core.payment.PopmoneyAccount; +import bisq.core.payment.PromptPayAccount; +import bisq.core.payment.RevolutAccount; +import bisq.core.payment.RtgsAccount; +import bisq.core.payment.SameBankAccount; +import bisq.core.payment.SatispayAccount; +import bisq.core.payment.SepaAccount; +import bisq.core.payment.SepaInstantAccount; +import bisq.core.payment.SpecificBanksAccount; +import bisq.core.payment.StrikeAccount; +import bisq.core.payment.SwiftAccount; +import bisq.core.payment.SwishAccount; +import bisq.core.payment.TikkieAccount; import bisq.core.payment.TradeLimits; -import bisq.core.locale.CountryUtil; +import bisq.core.payment.TransferwiseAccount; +import bisq.core.payment.TransferwiseUsdAccount; +import bisq.core.payment.USPostalMoneyOrderAccount; +import bisq.core.payment.UpholdAccount; +import bisq.core.payment.UpiAccount; +import bisq.core.payment.VerseAccount; +import bisq.core.payment.WeChatPayAccount; +import bisq.core.payment.WesternUnionAccount; import bisq.core.locale.CurrencyUtil; -import bisq.core.locale.FiatCurrency; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; @@ -31,6 +79,7 @@ import org.bitcoinj.core.Coin; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import lombok.EqualsAndHashCode; @@ -82,7 +131,10 @@ public final class PaymentMethod implements PersistablePayload, Comparable ALL_ASSET_CODES = CurrencyUtil.getAllSortedFiatCurrencies().stream().map(FiatCurrency::getCode).collect(Collectors.toList()); - private static final List EUR_ASSET_CODES = Arrays.asList("EUR"); - private static final List GBP_ASSET_CODES = Arrays.asList("GBP"); - private static final List SEK_ASSET_CODES = Arrays.asList("SEK"); - private static final List USD_ASSET_CODES = Arrays.asList("USD"); - private static final List CAD_ASSET_CODES = Arrays.asList("CAD"); - private static final List CNY_ASSET_CODES = Arrays.asList("CNY"); - private static final List JPY_ASSET_CODES = Arrays.asList("JPY"); - private static final List AUD_ASSET_CODES = Arrays.asList("AUD"); - private static final List THB_ASSET_CODES = Arrays.asList("THB"); - private static final List AMZ_ASSET_CODES = CountryUtil.getAllAmazonGiftCardCountries().stream().map(country -> CurrencyUtil.getCurrencyByCountryCode(country.code).getCode()).collect(Collectors.toList()); - private static final List UPHOLD_ASSET_CODES = CurrencyUtil.getAllUpholdCurrencies().stream().map(TradeCurrency::getCode).collect(Collectors.toList()); - private static final List REVOLUT_ASSET_CODES = CurrencyUtil.getAllRevolutCurrencies().stream().map(TradeCurrency::getCode).collect(Collectors.toList()); - private static final List PERFECT_MONEY_ASSET_CODES = Arrays.asList("USD", "EUR"); - private static final List ADVANCED_CASH_ASSET_CODES = CurrencyUtil.getAllAdvancedCashCurrencies().stream().map(TradeCurrency::getCode).collect(Collectors.toList()); - private static final List TRANSFERWISE_ASSET_CODES = CurrencyUtil.getAllTransferwiseCurrencies().stream().map(TradeCurrency::getCode).collect(Collectors.toList()); - private static final List EMPTY_ASSET_CODES = Arrays.asList(); + public static final String CAPITUAL_ID = "CAPITUAL"; + public static final String CELPAY_ID = "CELPAY"; + public static final String MONESE_ID = "MONESE"; + public static final String SATISPAY_ID = "SATISPAY"; + public static final String TIKKIE_ID = "TIKKIE"; + public static final String VERSE_ID = "VERSE"; + public static final String STRIKE_ID = "STRIKE"; + public static final String SWIFT_ID = "SWIFT"; + public static final String ACH_TRANSFER_ID = "ACH_TRANSFER"; + public static final String DOMESTIC_WIRE_TRANSFER_ID = "DOMESTIC_WIRE_TRANSFER"; // Cannot be deleted as it would break old trade history entries @Deprecated @@ -153,9 +208,31 @@ public final class PaymentMethod implements PersistablePayload, Comparable paymentMethods = new ArrayList<>(Arrays.asList( // EUR - HAL_CASH = new PaymentMethod(HAL_CASH_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, EUR_ASSET_CODES), - SEPA = new PaymentMethod(SEPA_ID, 6 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, EUR_ASSET_CODES), - SEPA_INSTANT = new PaymentMethod(SEPA_INSTANT_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, EUR_ASSET_CODES), - MONEY_BEAM = new PaymentMethod(MONEY_BEAM_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, EUR_ASSET_CODES), + HAL_CASH = new PaymentMethod(HAL_CASH_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(HalCashAccount.SUPPORTED_CURRENCIES)), + SEPA = new PaymentMethod(SEPA_ID, 6 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(SepaAccount.SUPPORTED_CURRENCIES)), + SEPA_INSTANT = new PaymentMethod(SEPA_INSTANT_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(SepaInstantAccount.SUPPORTED_CURRENCIES)), + MONEY_BEAM = new PaymentMethod(MONEY_BEAM_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(MoneyBeamAccount.SUPPORTED_CURRENCIES)), // UK - FASTER_PAYMENTS = new PaymentMethod(FASTER_PAYMENTS_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, GBP_ASSET_CODES), + FASTER_PAYMENTS = new PaymentMethod(FASTER_PAYMENTS_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(FasterPaymentsAccount.SUPPORTED_CURRENCIES)), // Sweden - SWISH = new PaymentMethod(SWISH_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, SEK_ASSET_CODES), + SWISH = new PaymentMethod(SWISH_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(SwishAccount.SUPPORTED_CURRENCIES)), // US - CLEAR_X_CHANGE = new PaymentMethod(CLEAR_X_CHANGE_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, USD_ASSET_CODES), + CLEAR_X_CHANGE = new PaymentMethod(CLEAR_X_CHANGE_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(ClearXchangeAccount.SUPPORTED_CURRENCIES)), - POPMONEY = new PaymentMethod(POPMONEY_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, USD_ASSET_CODES), - CHASE_QUICK_PAY = new PaymentMethod(CHASE_QUICK_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, USD_ASSET_CODES), - US_POSTAL_MONEY_ORDER = new PaymentMethod(US_POSTAL_MONEY_ORDER_ID, 8 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, USD_ASSET_CODES), + POPMONEY = new PaymentMethod(POPMONEY_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PopmoneyAccount.SUPPORTED_CURRENCIES)), + US_POSTAL_MONEY_ORDER = new PaymentMethod(US_POSTAL_MONEY_ORDER_ID, 8 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(USPostalMoneyOrderAccount.SUPPORTED_CURRENCIES)), // Canada - INTERAC_E_TRANSFER = new PaymentMethod(INTERAC_E_TRANSFER_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, CAD_ASSET_CODES), + INTERAC_E_TRANSFER = new PaymentMethod(INTERAC_E_TRANSFER_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(InteracETransferAccount.SUPPORTED_CURRENCIES)), // Global - CASH_DEPOSIT = new PaymentMethod(CASH_DEPOSIT_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, ALL_ASSET_CODES), - CASH_BY_MAIL = new PaymentMethod(CASH_BY_MAIL_ID, 8 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, ALL_ASSET_CODES), - MONEY_GRAM = new PaymentMethod(MONEY_GRAM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, CurrencyUtil.getAllMoneyGramCurrencies().stream().map(TradeCurrency::getCode).collect(Collectors.toList())), - WESTERN_UNION = new PaymentMethod(WESTERN_UNION_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, ALL_ASSET_CODES), - NATIONAL_BANK = new PaymentMethod(NATIONAL_BANK_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, ALL_ASSET_CODES), - SAME_BANK = new PaymentMethod(SAME_BANK_ID, 2 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, ALL_ASSET_CODES), - SPECIFIC_BANKS = new PaymentMethod(SPECIFIC_BANKS_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, ALL_ASSET_CODES), - F2F = new PaymentMethod(F2F_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, ALL_ASSET_CODES), - AMAZON_GIFT_CARD = new PaymentMethod(AMAZON_GIFT_CARD_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, AMZ_ASSET_CODES), + CASH_DEPOSIT = new PaymentMethod(CASH_DEPOSIT_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashDepositAccount.SUPPORTED_CURRENCIES)), + CASH_BY_MAIL = new PaymentMethod(CASH_BY_MAIL_ID, 8 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashByMailAccount.SUPPORTED_CURRENCIES)), + MONEY_GRAM = new PaymentMethod(MONEY_GRAM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(MoneyGramAccount.SUPPORTED_CURRENCIES)), + WESTERN_UNION = new PaymentMethod(WESTERN_UNION_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(WesternUnionAccount.SUPPORTED_CURRENCIES)), + NATIONAL_BANK = new PaymentMethod(NATIONAL_BANK_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(NationalBankAccount.SUPPORTED_CURRENCIES)), + SAME_BANK = new PaymentMethod(SAME_BANK_ID, 2 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(SameBankAccount.SUPPORTED_CURRENCIES)), + SPECIFIC_BANKS = new PaymentMethod(SPECIFIC_BANKS_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(SpecificBanksAccount.SUPPORTED_CURRENCIES)), + F2F = new PaymentMethod(F2F_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(F2FAccount.SUPPORTED_CURRENCIES)), + AMAZON_GIFT_CARD = new PaymentMethod(AMAZON_GIFT_CARD_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(AmazonGiftCardAccount.SUPPORTED_CURRENCIES)), // Trans national - UPHOLD = new PaymentMethod(UPHOLD_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, UPHOLD_ASSET_CODES), - REVOLUT = new PaymentMethod(REVOLUT_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, REVOLUT_ASSET_CODES), - PERFECT_MONEY = new PaymentMethod(PERFECT_MONEY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, PERFECT_MONEY_ASSET_CODES), - ADVANCED_CASH = new PaymentMethod(ADVANCED_CASH_ID, DAY, DEFAULT_TRADE_LIMIT_VERY_LOW_RISK, ADVANCED_CASH_ASSET_CODES), - TRANSFERWISE = new PaymentMethod(TRANSFERWISE_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, TRANSFERWISE_ASSET_CODES), + UPHOLD = new PaymentMethod(UPHOLD_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(UpholdAccount.SUPPORTED_CURRENCIES)), + REVOLUT = new PaymentMethod(REVOLUT_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(RevolutAccount.SUPPORTED_CURRENCIES)), + PERFECT_MONEY = new PaymentMethod(PERFECT_MONEY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(PerfectMoneyAccount.SUPPORTED_CURRENCIES)), + ADVANCED_CASH = new PaymentMethod(ADVANCED_CASH_ID, DAY, DEFAULT_TRADE_LIMIT_VERY_LOW_RISK, getAssetCodes(AdvancedCashAccount.SUPPORTED_CURRENCIES)), + TRANSFERWISE = new PaymentMethod(TRANSFERWISE_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(TransferwiseAccount.SUPPORTED_CURRENCIES)), + TRANSFERWISE_USD = new PaymentMethod(TRANSFERWISE_USD_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(TransferwiseUsdAccount.SUPPORTED_CURRENCIES)), + PAYSERA = new PaymentMethod(PAYSERA_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PayseraAccount.SUPPORTED_CURRENCIES)), + PAXUM = new PaymentMethod(PAXUM_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PaxumAccount.SUPPORTED_CURRENCIES)), + NEFT = new PaymentMethod(NEFT_ID, DAY, Coin.parseCoin("0.02"), getAssetCodes(NeftAccount.SUPPORTED_CURRENCIES)), + RTGS = new PaymentMethod(RTGS_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(RtgsAccount.SUPPORTED_CURRENCIES)), + IMPS = new PaymentMethod(IMPS_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(ImpsAccount.SUPPORTED_CURRENCIES)), + UPI = new PaymentMethod(UPI_ID, DAY, Coin.parseCoin("0.05"), getAssetCodes(UpiAccount.SUPPORTED_CURRENCIES)), + PAYTM = new PaymentMethod(PAYTM_ID, DAY, Coin.parseCoin("0.05"), getAssetCodes(PaytmAccount.SUPPORTED_CURRENCIES)), + NEQUI = new PaymentMethod(NEQUI_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(NequiAccount.SUPPORTED_CURRENCIES)), + BIZUM = new PaymentMethod(BIZUM_ID, DAY, Coin.parseCoin("0.04"), getAssetCodes(BizumAccount.SUPPORTED_CURRENCIES)), + PIX = new PaymentMethod(PIX_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PixAccount.SUPPORTED_CURRENCIES)), + CAPITUAL = new PaymentMethod(CAPITUAL_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CapitualAccount.SUPPORTED_CURRENCIES)), + CELPAY = new PaymentMethod(CELPAY_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CelPayAccount.SUPPORTED_CURRENCIES)), + MONESE = new PaymentMethod(MONESE_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(MoneseAccount.SUPPORTED_CURRENCIES)), + SATISPAY = new PaymentMethod(SATISPAY_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(SatispayAccount.SUPPORTED_CURRENCIES)), + TIKKIE = new PaymentMethod(TIKKIE_ID, DAY, Coin.parseCoin("0.05"), getAssetCodes(TikkieAccount.SUPPORTED_CURRENCIES)), + VERSE = new PaymentMethod(VERSE_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(VerseAccount.SUPPORTED_CURRENCIES)), + STRIKE = new PaymentMethod(STRIKE_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(StrikeAccount.SUPPORTED_CURRENCIES)), + SWIFT = new PaymentMethod(SWIFT_ID, 7 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(SwiftAccount.SUPPORTED_CURRENCIES)), + ACH_TRANSFER = new PaymentMethod(ACH_TRANSFER_ID, 5 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(AchTransferAccount.SUPPORTED_CURRENCIES)), + DOMESTIC_WIRE_TRANSFER = new PaymentMethod(DOMESTIC_WIRE_TRANSFER_ID, 3 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(DomesticWireTransferAccount.SUPPORTED_CURRENCIES)), // Japan - JAPAN_BANK = new PaymentMethod(JAPAN_BANK_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, JPY_ASSET_CODES), + JAPAN_BANK = new PaymentMethod(JAPAN_BANK_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(JapanBankAccount.SUPPORTED_CURRENCIES)), // Australia - AUSTRALIA_PAYID = new PaymentMethod(AUSTRALIA_PAYID_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, AUD_ASSET_CODES), + AUSTRALIA_PAYID = new PaymentMethod(AUSTRALIA_PAYID_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(AustraliaPayidAccount.SUPPORTED_CURRENCIES)), // China - ALI_PAY = new PaymentMethod(ALI_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, CNY_ASSET_CODES), - WECHAT_PAY = new PaymentMethod(WECHAT_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, CNY_ASSET_CODES), + ALI_PAY = new PaymentMethod(ALI_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(AliPayAccount.SUPPORTED_CURRENCIES)), + WECHAT_PAY = new PaymentMethod(WECHAT_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(WeChatPayAccount.SUPPORTED_CURRENCIES)), // Thailand - PROMPT_PAY = new PaymentMethod(PROMPT_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, THB_ASSET_CODES), + PROMPT_PAY = new PaymentMethod(PROMPT_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(PromptPayAccount.SUPPORTED_CURRENCIES)), // Altcoins - BLOCK_CHAINS = new PaymentMethod(BLOCK_CHAINS_ID, DAY, DEFAULT_TRADE_LIMIT_VERY_LOW_RISK, EMPTY_ASSET_CODES), + BLOCK_CHAINS = new PaymentMethod(BLOCK_CHAINS_ID, DAY, DEFAULT_TRADE_LIMIT_VERY_LOW_RISK, Arrays.asList()), // Altcoins with 1 hour trade period - BLOCK_CHAINS_INSTANT = new PaymentMethod(BLOCK_CHAINS_INSTANT_ID, TimeUnit.HOURS.toMillis(1), DEFAULT_TRADE_LIMIT_VERY_LOW_RISK, EMPTY_ASSET_CODES) + BLOCK_CHAINS_INSTANT = new PaymentMethod(BLOCK_CHAINS_INSTANT_ID, TimeUnit.HOURS.toMillis(1), DEFAULT_TRADE_LIMIT_VERY_LOW_RISK, Arrays.asList()) )); + private static List getAssetCodes(List tradeCurrencies) { + return tradeCurrencies.stream().map(TradeCurrency::getCode).collect(Collectors.toList()); + } + static { paymentMethods.sort((o1, o2) -> { String id1 = o1.getId(); @@ -241,7 +342,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable supportedAssetCodes) { this.id = id; @@ -319,17 +421,26 @@ public final class PaymentMethod implements PersistablePayload, Comparable new PaymentMethod(Res.get("shared.na"))); + } + + // We look up only our active payment methods not retired ones. + public static Optional getActivePaymentMethod(String id) { return paymentMethods.stream() .filter(e -> e.getId().equals(id)) - .findFirst() - .orElseGet(() -> new PaymentMethod(Res.get("shared.na"))); + .findFirst(); } public Coin getMaxTradeLimitAsCoin(String currencyCode) { // Hack for SF as the smallest unit is 1 SF ;-( and price is about 3 BTC! if (currencyCode.equals("SF")) return Coin.parseCoin("4"); + // payment methods which define their own trade limits + if (id.equals(NEFT_ID) || id.equals(UPI_ID) || id.equals(PAYTM_ID) || id.equals(BIZUM_ID) || id.equals(TIKKIE_ID)) { + return Coin.valueOf(maxTradeLimit); + } // We use the class field maxTradeLimit only for mapping the risk factor. long riskFactor; @@ -343,7 +454,9 @@ public final class PaymentMethod implements PersistablePayload, Comparable tradeCurrencies) { return tradeCurrencies.stream() .anyMatch(tradeCurrency -> hasChargebackRisk(paymentMethod, tradeCurrency.getCode())); diff --git a/core/src/main/java/bisq/core/payment/payload/PayseraAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/PayseraAccountPayload.java new file mode 100644 index 0000000000..e26710c212 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/PayseraAccountPayload.java @@ -0,0 +1,99 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class PayseraAccountPayload extends PaymentAccountPayload { + private String email = ""; + + public PayseraAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private PayseraAccountPayload(String paymentMethod, + String id, + String email, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.email = email; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setPayseraAccountPayload(protobuf.PayseraAccountPayload.newBuilder().setEmail(email)) + .build(); + } + + public static PayseraAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new PayseraAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getPayseraAccountPayload().getEmail(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email") + " " + email; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(email.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/PaytmAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/PaytmAccountPayload.java new file mode 100644 index 0000000000..68c7878fce --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/PaytmAccountPayload.java @@ -0,0 +1,99 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class PaytmAccountPayload extends CountryBasedPaymentAccountPayload { + private String emailOrMobileNr = ""; + + public PaytmAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private PaytmAccountPayload(String paymentMethod, + String id, + String countryCode, + String emailOrMobileNr, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + + this.emailOrMobileNr = emailOrMobileNr; + } + + @Override + public Message toProtoMessage() { + protobuf.PaytmAccountPayload.Builder builder = protobuf.PaytmAccountPayload.newBuilder() + .setEmailOrMobileNr(emailOrMobileNr); + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setPaytmAccountPayload(builder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) + .build(); + } + + public static PaytmAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.PaytmAccountPayload paytmAccountPayloadPB = countryBasedPaymentAccountPayload.getPaytmAccountPayload(); + return new PaytmAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + paytmAccountPayloadPB.getEmailOrMobileNr(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email.mobile") + " " + emailOrMobileNr; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(emailOrMobileNr.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/PixAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/PixAccountPayload.java new file mode 100644 index 0000000000..7c307abeec --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/PixAccountPayload.java @@ -0,0 +1,99 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class PixAccountPayload extends CountryBasedPaymentAccountPayload { + private String pixKey = ""; + + public PixAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private PixAccountPayload(String paymentMethod, + String id, + String countryCode, + String pixKey, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + + this.pixKey = pixKey; + } + + @Override + public Message toProtoMessage() { + protobuf.PixAccountPayload.Builder builder = protobuf.PixAccountPayload.newBuilder() + .setPixKey(pixKey); + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setPixAccountPayload(builder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) + .build(); + } + + public static PixAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.PixAccountPayload paytmAccountPayloadPB = countryBasedPaymentAccountPayload.getPixAccountPayload(); + return new PixAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + paytmAccountPayloadPB.getPixKey(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.pix.key") + " " + pixKey; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(pixKey.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/RtgsAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/RtgsAccountPayload.java new file mode 100644 index 0000000000..177e521e16 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/RtgsAccountPayload.java @@ -0,0 +1,115 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class RtgsAccountPayload extends IfscBasedAccountPayload { + + public RtgsAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private RtgsAccountPayload(String paymentMethod, + String id, + String countryCode, + String holderName, + String accountNr, + String ifsc, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + countryCode, + holderName, + accountNr, + ifsc, + maxTradePeriod, + excludeFromJsonDataMap); + } + + @Override + public Message toProtoMessage() { + protobuf.IfscBasedAccountPayload.Builder ifscBasedAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .getIfscBasedAccountPayloadBuilder() + .setRtgsAccountPayload(protobuf.RtgsAccountPayload.newBuilder()); + + protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setIfscBasedAccountPayload(ifscBasedAccountPayloadBuilder); + + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) + .build(); + } + + public static RtgsAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.IfscBasedAccountPayload ifscBasedAccountPayloadPB = countryBasedPaymentAccountPayload.getIfscBasedAccountPayload(); + return new RtgsAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + ifscBasedAccountPayloadPB.getHolderName(), + ifscBasedAccountPayloadPB.getAccountNr(), + ifscBasedAccountPayloadPB.getIfsc(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " + + Res.getWithCol("payment.account.no") + " " + accountNr + + Res.getWithCol("payment.ifsc") + " " + ifsc; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + String accountNr = this.accountNr == null ? "" : this.accountNr; + return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getHolderName() { + return getOwnerId(); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/SatispayAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SatispayAccountPayload.java new file mode 100644 index 0000000000..0e4782ee43 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/SatispayAccountPayload.java @@ -0,0 +1,104 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class SatispayAccountPayload extends CountryBasedPaymentAccountPayload { + private String holderName = ""; + private String mobileNr = ""; + + public SatispayAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private SatispayAccountPayload(String paymentMethod, + String id, + String countryCode, + String holderName, + String mobileNr, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + + this.holderName = holderName; + this.mobileNr = mobileNr; + } + + @Override + public Message toProtoMessage() { + protobuf.SatispayAccountPayload.Builder builder = protobuf.SatispayAccountPayload.newBuilder() + .setHolderName(holderName) + .setMobileNr(mobileNr); + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setSatispayAccountPayload(builder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) + .build(); + } + + public static SatispayAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.SatispayAccountPayload accountPayloadPB = countryBasedPaymentAccountPayload.getSatispayAccountPayload(); + return new SatispayAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + accountPayloadPB.getHolderName(), + accountPayloadPB.getMobileNr(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.userName") + " " + holderName; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(holderName.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java index 3d13c9046d..6c283209e7 100644 --- a/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java @@ -54,6 +54,7 @@ public final class SepaAccountPayload extends CountryBasedPaymentAccountPayload // Don't use a set here as we need a deterministic ordering, otherwise the contract hash does not match private final List acceptedCountryCodes; + private final List persistedAcceptedCountryCodes = new ArrayList<>(); public SepaAccountPayload(String paymentMethod, String id, List acceptedCountries) { super(paymentMethod, id); @@ -90,6 +91,7 @@ public final class SepaAccountPayload extends CountryBasedPaymentAccountPayload this.bic = bic; this.email = email; this.acceptedCountryCodes = acceptedCountryCodes; + persistedAcceptedCountryCodes.addAll(acceptedCountryCodes); } @Override @@ -138,6 +140,16 @@ public final class SepaAccountPayload extends CountryBasedPaymentAccountPayload acceptedCountryCodes.remove(countryCode); } + public void onPersistChanges() { + persistedAcceptedCountryCodes.clear(); + persistedAcceptedCountryCodes.addAll(acceptedCountryCodes); + } + + public void revertChanges() { + acceptedCountryCodes.clear(); + acceptedCountryCodes.addAll(persistedAcceptedCountryCodes); + } + @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", IBAN: " + diff --git a/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java index 38636cc149..e631da5924 100644 --- a/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java @@ -53,6 +53,7 @@ public final class SepaInstantAccountPayload extends CountryBasedPaymentAccountP // Don't use a set here as we need a deterministic ordering, otherwise the contract hash does not match private final List acceptedCountryCodes; + private final List persistedAcceptedCountryCodes = new ArrayList<>(); public SepaInstantAccountPayload(String paymentMethod, String id, List acceptedCountries) { super(paymentMethod, id); @@ -87,6 +88,7 @@ public final class SepaInstantAccountPayload extends CountryBasedPaymentAccountP this.iban = iban; this.bic = bic; this.acceptedCountryCodes = acceptedCountryCodes; + persistedAcceptedCountryCodes.addAll(acceptedCountryCodes); } @Override @@ -133,6 +135,16 @@ public final class SepaInstantAccountPayload extends CountryBasedPaymentAccountP acceptedCountryCodes.remove(countryCode); } + public void onPersistChanges() { + persistedAcceptedCountryCodes.clear(); + persistedAcceptedCountryCodes.addAll(acceptedCountryCodes); + } + + public void revertChanges() { + acceptedCountryCodes.clear(); + acceptedCountryCodes.addAll(persistedAcceptedCountryCodes); + } + @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", IBAN: " + diff --git a/core/src/main/java/bisq/core/payment/payload/StrikeAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/StrikeAccountPayload.java new file mode 100644 index 0000000000..d9744af049 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/StrikeAccountPayload.java @@ -0,0 +1,99 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class StrikeAccountPayload extends CountryBasedPaymentAccountPayload { + private String holderName = ""; + + public StrikeAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private StrikeAccountPayload(String paymentMethod, + String id, + String countryCode, + String holderName, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + + this.holderName = holderName; + } + + @Override + public Message toProtoMessage() { + protobuf.StrikeAccountPayload.Builder builder = protobuf.StrikeAccountPayload.newBuilder() + .setHolderName(holderName); + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setStrikeAccountPayload(builder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) + .build(); + } + + public static StrikeAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.StrikeAccountPayload accountPayloadPB = countryBasedPaymentAccountPayload.getStrikeAccountPayload(); + return new StrikeAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + accountPayloadPB.getHolderName(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.userName") + " " + holderName; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(holderName.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/SwiftAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SwiftAccountPayload.java new file mode 100644 index 0000000000..4c8cc98675 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/SwiftAccountPayload.java @@ -0,0 +1,184 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class SwiftAccountPayload extends PaymentAccountPayload { + // payload data elements + private String bankSwiftCode = ""; + private String bankCountryCode = ""; + private String bankName = ""; + private String bankBranch = ""; + private String bankAddress = ""; + private String beneficiaryName = ""; + private String beneficiaryAccountNr = ""; + private String beneficiaryAddress = ""; + private String beneficiaryCity = ""; + private String beneficiaryPhone = ""; + private String specialInstructions = ""; + private String intermediarySwiftCode = ""; + private String intermediaryCountryCode = ""; + private String intermediaryName = ""; + private String intermediaryBranch = ""; + private String intermediaryAddress = ""; + + // constants + public static final String BANKPOSTFIX = ".bank"; + public static final String INTERMEDIARYPOSTFIX = ".intermediary"; + public static final String BENEFICIARYPOSTFIX = ".beneficiary"; + public static final String SWIFT_CODE = "payment.swift.swiftCode"; + public static final String COUNTRY = "payment.swift.country"; + public static final String SWIFT_ACCOUNT = "payment.swift.account"; + public static final String SNAME = "payment.swift.name"; + public static final String BRANCH = "payment.swift.branch"; + public static final String ADDRESS = "payment.swift.address"; + public static final String PHONE = "payment.swift.phone"; + + public SwiftAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private SwiftAccountPayload(String paymentMethod, + String id, + String bankSwiftCode, + String bankCountryCode, + String bankName, + String bankBranch, + String bankAddress, + String beneficiaryName, + String beneficiaryAccountNr, + String beneficiaryAddress, + String beneficiaryCity, + String beneficiaryPhone, + String specialInstructions, + String intermediarySwiftCode, + String intermediaryCountryCode, + String intermediaryName, + String intermediaryBranch, + String intermediaryAddress, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.bankSwiftCode = bankSwiftCode; + this.bankCountryCode = bankCountryCode; + this.bankName = bankName; + this.bankBranch = bankBranch; + this.bankAddress = bankAddress; + this.beneficiaryName = beneficiaryName; + this.beneficiaryAccountNr = beneficiaryAccountNr; + this.beneficiaryAddress = beneficiaryAddress; + this.beneficiaryCity = beneficiaryCity; + this.beneficiaryPhone = beneficiaryPhone; + this.specialInstructions = specialInstructions; + this.intermediarySwiftCode = intermediarySwiftCode; + this.intermediaryCountryCode = intermediaryCountryCode; + this.intermediaryName = intermediaryName; + this.intermediaryBranch = intermediaryBranch; + this.intermediaryAddress = intermediaryAddress; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setSwiftAccountPayload(protobuf.SwiftAccountPayload.newBuilder() + .setBankSwiftCode(bankSwiftCode) + .setBankCountryCode(bankCountryCode) + .setBankName(bankName) + .setBankBranch(bankBranch) + .setBankAddress(bankAddress) + .setBeneficiaryName(beneficiaryName) + .setBeneficiaryAccountNr(beneficiaryAccountNr) + .setBeneficiaryAddress(beneficiaryAddress) + .setBeneficiaryCity(beneficiaryCity) + .setBeneficiaryPhone(beneficiaryPhone) + .setSpecialInstructions(specialInstructions) + .setIntermediarySwiftCode(intermediarySwiftCode) + .setIntermediaryCountryCode(intermediaryCountryCode) + .setIntermediaryName(intermediaryName) + .setIntermediaryBranch(intermediaryBranch) + .setIntermediaryAddress(intermediaryAddress) + ) + .build(); + } + + public static SwiftAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.SwiftAccountPayload x = proto.getSwiftAccountPayload(); + return new SwiftAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + x.getBankSwiftCode(), + x.getBankCountryCode(), + x.getBankName(), + x.getBankBranch(), + x.getBankAddress(), + x.getBeneficiaryName(), + x.getBeneficiaryAccountNr(), + x.getBeneficiaryAddress(), + x.getBeneficiaryCity(), + x.getBeneficiaryPhone(), + x.getSpecialInstructions(), + x.getIntermediarySwiftCode(), + x.getIntermediaryCountryCode(), + x.getIntermediaryName(), + x.getIntermediaryBranch(), + x.getIntermediaryAddress(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + beneficiaryName; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(beneficiaryAccountNr.getBytes(StandardCharsets.UTF_8)); + } + + public boolean usesIntermediaryBank() { + return (intermediarySwiftCode != null && intermediarySwiftCode.length() > 0); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/TikkieAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/TikkieAccountPayload.java new file mode 100644 index 0000000000..ce13e00198 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/TikkieAccountPayload.java @@ -0,0 +1,99 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class TikkieAccountPayload extends CountryBasedPaymentAccountPayload { + private String iban = ""; + + public TikkieAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private TikkieAccountPayload(String paymentMethod, + String id, + String countryCode, + String iban, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + + this.iban = iban; + } + + @Override + public Message toProtoMessage() { + protobuf.TikkieAccountPayload.Builder builder = protobuf.TikkieAccountPayload.newBuilder() + .setIban(iban); + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setTikkieAccountPayload(builder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) + .build(); + } + + public static TikkieAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.TikkieAccountPayload accountPayloadPB = countryBasedPaymentAccountPayload.getTikkieAccountPayload(); + return new TikkieAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + accountPayloadPB.getIban(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.iban") + " " + iban; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(iban.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/TransferwiseUsdAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/TransferwiseUsdAccountPayload.java new file mode 100644 index 0000000000..6fc4f025e7 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/TransferwiseUsdAccountPayload.java @@ -0,0 +1,109 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAccountPayload { + private String email = ""; + private String holderName = ""; + private String beneficiaryAddress = ""; + + public TransferwiseUsdAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private TransferwiseUsdAccountPayload(String paymentMethod, + String id, + String countryCode, + String email, + String holderName, + String beneficiaryAddress, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + + this.email = email; + this.holderName = holderName; + this.beneficiaryAddress = beneficiaryAddress; + } + + @Override + public Message toProtoMessage() { + protobuf.TransferwiseUsdAccountPayload.Builder builder = protobuf.TransferwiseUsdAccountPayload.newBuilder() + .setEmail(email) + .setHolderName(holderName) + .setBeneficiaryAddress(beneficiaryAddress); + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setTransferwiseUsdAccountPayload(builder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) + .build(); + } + + public static TransferwiseUsdAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.TransferwiseUsdAccountPayload accountPayloadPB = countryBasedPaymentAccountPayload.getTransferwiseUsdAccountPayload(); + return new TransferwiseUsdAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + accountPayloadPB.getEmail(), + accountPayloadPB.getHolderName(), + accountPayloadPB.getBeneficiaryAddress(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.userName") + " " + holderName; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(holderName.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/UpholdAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/UpholdAccountPayload.java index ba4a8154e0..161fb37a34 100644 --- a/core/src/main/java/bisq/core/payment/payload/UpholdAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/UpholdAccountPayload.java @@ -19,6 +19,8 @@ package bisq.core.payment.payload; import bisq.core.locale.Res; +import bisq.common.util.JsonExclude; + import com.google.protobuf.Message; import java.nio.charset.StandardCharsets; @@ -40,6 +42,10 @@ import lombok.extern.slf4j.Slf4j; public final class UpholdAccountPayload extends PaymentAccountPayload { private String accountId = ""; + // For backward compatibility we need to exclude the new field from the contract json. + @JsonExclude + private String accountOwner = ""; + public UpholdAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } @@ -52,6 +58,7 @@ public final class UpholdAccountPayload extends PaymentAccountPayload { private UpholdAccountPayload(String paymentMethod, String id, String accountId, + String accountOwner, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, @@ -60,12 +67,14 @@ public final class UpholdAccountPayload extends PaymentAccountPayload { excludeFromJsonDataMap); this.accountId = accountId; + this.accountOwner = accountOwner; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setUpholdAccountPayload(protobuf.UpholdAccountPayload.newBuilder() + .setAccountOwner(accountOwner) .setAccountId(accountId)) .build(); } @@ -74,6 +83,7 @@ public final class UpholdAccountPayload extends PaymentAccountPayload { return new UpholdAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getUpholdAccountPayload().getAccountId(), + proto.getUpholdAccountPayload().getAccountOwner(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @@ -85,12 +95,20 @@ public final class UpholdAccountPayload extends PaymentAccountPayload { @Override public String getPaymentDetails() { - return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account") + " " + accountId; + return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } @Override public String getPaymentDetailsForTradePopup() { - return getPaymentDetails(); + if (accountOwner.isEmpty()) { + return + Res.get("payment.account") + ": " + accountId + "\n" + + Res.get("payment.account.owner") + ": N/A"; + } else { + return + Res.get("payment.account") + ": " + accountId + "\n" + + Res.get("payment.account.owner") + ": " + accountOwner; + } } @Override diff --git a/core/src/main/java/bisq/core/payment/payload/UpiAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/UpiAccountPayload.java new file mode 100644 index 0000000000..4a46c856ae --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/UpiAccountPayload.java @@ -0,0 +1,99 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class UpiAccountPayload extends CountryBasedPaymentAccountPayload { + private String virtualPaymentAddress = ""; + + public UpiAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private UpiAccountPayload(String paymentMethod, + String id, + String countryCode, + String virtualPaymentAddress, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + countryCode, + maxTradePeriod, + excludeFromJsonDataMap); + + this.virtualPaymentAddress = virtualPaymentAddress; + } + + @Override + public Message toProtoMessage() { + protobuf.UpiAccountPayload.Builder builder = protobuf.UpiAccountPayload.newBuilder() + .setVirtualPaymentAddress(virtualPaymentAddress); + final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() + .getCountryBasedPaymentAccountPayloadBuilder() + .setUpiAccountPayload(builder); + return getPaymentAccountPayloadBuilder() + .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) + .build(); + } + + public static UpiAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); + protobuf.UpiAccountPayload upiAccountPayloadPB = countryBasedPaymentAccountPayload.getUpiAccountPayload(); + return new UpiAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + countryBasedPaymentAccountPayload.getCountryCode(), + upiAccountPayloadPB.getVirtualPaymentAddress(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.upi.virtualPaymentAddress") + " " + virtualPaymentAddress; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(virtualPaymentAddress.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/VerseAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/VerseAccountPayload.java new file mode 100644 index 0000000000..d9344ef2c0 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/VerseAccountPayload.java @@ -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 . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class VerseAccountPayload extends PaymentAccountPayload { + private String holderName = ""; + + public VerseAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + private VerseAccountPayload(String paymentMethod, + String id, + String holderName, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.holderName = holderName; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setVerseAccountPayload(protobuf.VerseAccountPayload.newBuilder().setHolderName(holderName)) + .build(); + } + + public static VerseAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new VerseAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getVerseAccountPayload().getHolderName(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.userName") + " " + holderName; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(holderName.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/proto/CoreProtoResolver.java b/core/src/main/java/bisq/core/proto/CoreProtoResolver.java index 2f1f65a1ae..e1e5307999 100644 --- a/core/src/main/java/bisq/core/proto/CoreProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/CoreProtoResolver.java @@ -19,40 +19,61 @@ package bisq.core.proto; import bisq.core.account.sign.SignedWitness; import bisq.core.account.witness.AccountAgeWitness; +import bisq.core.payment.payload.AchTransferAccountPayload; import bisq.core.payment.payload.AdvancedCashAccountPayload; import bisq.core.payment.payload.AliPayAccountPayload; import bisq.core.payment.payload.AmazonGiftCardAccountPayload; -import bisq.core.payment.payload.AustraliaPayidPayload; +import bisq.core.payment.payload.AustraliaPayidAccountPayload; +import bisq.core.payment.payload.BizumAccountPayload; +import bisq.core.payment.payload.CapitualAccountPayload; import bisq.core.payment.payload.CashAppAccountPayload; import bisq.core.payment.payload.CashByMailAccountPayload; import bisq.core.payment.payload.CashDepositAccountPayload; +import bisq.core.payment.payload.CelPayAccountPayload; import bisq.core.payment.payload.ChaseQuickPayAccountPayload; import bisq.core.payment.payload.ClearXchangeAccountPayload; import bisq.core.payment.payload.CryptoCurrencyAccountPayload; +import bisq.core.payment.payload.DomesticWireTransferAccountPayload; import bisq.core.payment.payload.F2FAccountPayload; import bisq.core.payment.payload.FasterPaymentsAccountPayload; import bisq.core.payment.payload.HalCashAccountPayload; +import bisq.core.payment.payload.ImpsAccountPayload; import bisq.core.payment.payload.InstantCryptoCurrencyPayload; import bisq.core.payment.payload.InteracETransferAccountPayload; import bisq.core.payment.payload.JapanBankAccountPayload; +import bisq.core.payment.payload.MoneseAccountPayload; import bisq.core.payment.payload.MoneyBeamAccountPayload; import bisq.core.payment.payload.MoneyGramAccountPayload; import bisq.core.payment.payload.NationalBankAccountPayload; +import bisq.core.payment.payload.NeftAccountPayload; +import bisq.core.payment.payload.NequiAccountPayload; import bisq.core.payment.payload.OKPayAccountPayload; +import bisq.core.payment.payload.PaxumAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PayseraAccountPayload; +import bisq.core.payment.payload.PaytmAccountPayload; import bisq.core.payment.payload.PerfectMoneyAccountPayload; +import bisq.core.payment.payload.PixAccountPayload; import bisq.core.payment.payload.PopmoneyAccountPayload; import bisq.core.payment.payload.PromptPayAccountPayload; import bisq.core.payment.payload.RevolutAccountPayload; +import bisq.core.payment.payload.RtgsAccountPayload; import bisq.core.payment.payload.SameBankAccountPayload; +import bisq.core.payment.payload.SatispayAccountPayload; import bisq.core.payment.payload.SepaAccountPayload; import bisq.core.payment.payload.SepaInstantAccountPayload; import bisq.core.payment.payload.SpecificBanksAccountPayload; +import bisq.core.payment.payload.StrikeAccountPayload; +import bisq.core.payment.payload.SwiftAccountPayload; import bisq.core.payment.payload.SwishAccountPayload; +import bisq.core.payment.payload.TikkieAccountPayload; import bisq.core.payment.payload.TransferwiseAccountPayload; +import bisq.core.payment.payload.TransferwiseUsdAccountPayload; import bisq.core.payment.payload.USPostalMoneyOrderAccountPayload; import bisq.core.payment.payload.UpholdAccountPayload; +import bisq.core.payment.payload.UpiAccountPayload; import bisq.core.payment.payload.VenmoAccountPayload; +import bisq.core.payment.payload.VerseAccountPayload; import bisq.core.payment.payload.WeChatPayAccountPayload; import bisq.core.payment.payload.WesternUnionAccountPayload; import bisq.core.trade.statistics.TradeStatistics2; @@ -97,6 +118,10 @@ public class CoreProtoResolver implements ProtoResolver { return SameBankAccountPayload.fromProto(proto); case SPECIFIC_BANKS_ACCOUNT_PAYLOAD: return SpecificBanksAccountPayload.fromProto(proto); + case ACH_TRANSFER_ACCOUNT_PAYLOAD: + return AchTransferAccountPayload.fromProto(proto); + case DOMESTIC_WIRE_TRANSFER_ACCOUNT_PAYLOAD: + return DomesticWireTransferAccountPayload.fromProto(proto); default: throw new ProtobufferRuntimeException("Unknown proto message case" + "(PB.PaymentAccountPayload.CountryBasedPaymentAccountPayload.BankAccountPayload). " + @@ -112,6 +137,38 @@ public class CoreProtoResolver implements ProtoResolver { return SepaInstantAccountPayload.fromProto(proto); case F2F_ACCOUNT_PAYLOAD: return F2FAccountPayload.fromProto(proto); + case UPI_ACCOUNT_PAYLOAD: + return UpiAccountPayload.fromProto(proto); + case PAYTM_ACCOUNT_PAYLOAD: + return PaytmAccountPayload.fromProto(proto); + case NEQUI_ACCOUNT_PAYLOAD: + return NequiAccountPayload.fromProto(proto); + case BIZUM_ACCOUNT_PAYLOAD: + return BizumAccountPayload.fromProto(proto); + case PIX_ACCOUNT_PAYLOAD: + return PixAccountPayload.fromProto(proto); + case SATISPAY_ACCOUNT_PAYLOAD: + return SatispayAccountPayload.fromProto(proto); + case TIKKIE_ACCOUNT_PAYLOAD: + return TikkieAccountPayload.fromProto(proto); + case STRIKE_ACCOUNT_PAYLOAD: + return StrikeAccountPayload.fromProto(proto); + case TRANSFERWISE_USD_ACCOUNT_PAYLOAD: + return TransferwiseUsdAccountPayload.fromProto(proto); + case IFSC_BASED_ACCOUNT_PAYLOAD: + final protobuf.IfscBasedAccountPayload.MessageCase messageCaseIfsc = proto.getCountryBasedPaymentAccountPayload().getIfscBasedAccountPayload().getMessageCase(); + switch (messageCaseIfsc) { + case NEFT_ACCOUNT_PAYLOAD: + return NeftAccountPayload.fromProto(proto); + case RTGS_ACCOUNT_PAYLOAD: + return RtgsAccountPayload.fromProto(proto); + case IMPS_ACCOUNT_PAYLOAD: + return ImpsAccountPayload.fromProto(proto); + default: + throw new ProtobufferRuntimeException("Unknown proto message case" + + "(PB.PaymentAccountPayload.CountryBasedPaymentAccountPayload.IfscBasedPaymentAccount). " + + "messageCase=" + messageCaseIfsc); + } default: throw new ProtobufferRuntimeException("Unknown proto message case" + "(PB.PaymentAccountPayload.CountryBasedPaymentAccountPayload)." + @@ -126,7 +183,7 @@ public class CoreProtoResolver implements ProtoResolver { case JAPAN_BANK_ACCOUNT_PAYLOAD: return JapanBankAccountPayload.fromProto(proto); case AUSTRALIA_PAYID_PAYLOAD: - return AustraliaPayidPayload.fromProto(proto); + return AustraliaPayidAccountPayload.fromProto(proto); case UPHOLD_ACCOUNT_PAYLOAD: return UpholdAccountPayload.fromProto(proto); case MONEY_BEAM_ACCOUNT_PAYLOAD: @@ -153,10 +210,24 @@ public class CoreProtoResolver implements ProtoResolver { return AdvancedCashAccountPayload.fromProto(proto); case TRANSFERWISE_ACCOUNT_PAYLOAD: return TransferwiseAccountPayload.fromProto(proto); + case PAYSERA_ACCOUNT_PAYLOAD: + return PayseraAccountPayload.fromProto(proto); + case PAXUM_ACCOUNT_PAYLOAD: + return PaxumAccountPayload.fromProto(proto); case AMAZON_GIFT_CARD_ACCOUNT_PAYLOAD: return AmazonGiftCardAccountPayload.fromProto(proto); case INSTANT_CRYPTO_CURRENCY_ACCOUNT_PAYLOAD: return InstantCryptoCurrencyPayload.fromProto(proto); + case CAPITUAL_ACCOUNT_PAYLOAD: + return CapitualAccountPayload.fromProto(proto); + case CEL_PAY_ACCOUNT_PAYLOAD: + return CelPayAccountPayload.fromProto(proto); + case MONESE_ACCOUNT_PAYLOAD: + return MoneseAccountPayload.fromProto(proto); + case VERSE_ACCOUNT_PAYLOAD: + return VerseAccountPayload.fromProto(proto); + case SWIFT_ACCOUNT_PAYLOAD: + return SwiftAccountPayload.fromProto(proto); // Cannot be deleted as it would break old trade history entries case O_K_PAY_ACCOUNT_PAYLOAD: diff --git a/core/src/main/java/bisq/core/proto/ProtoDevUtil.java b/core/src/main/java/bisq/core/proto/ProtoDevUtil.java index b1357e5a93..8808eb359e 100644 --- a/core/src/main/java/bisq/core/proto/ProtoDevUtil.java +++ b/core/src/main/java/bisq/core/proto/ProtoDevUtil.java @@ -21,6 +21,7 @@ import bisq.core.btc.model.AddressEntry; import bisq.core.support.dispute.DisputeResult; import bisq.core.offer.AvailabilityResult; import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferPayload; import bisq.core.offer.OpenOffer; import bisq.core.trade.Trade; @@ -81,8 +82,8 @@ public class ProtoDevUtil { sb.append(" enum Direction {\n"); sb.append(" PB_ERROR = 0;\n"); - for (int i = 0; i < OfferPayload.Direction.values().length; i++) { - OfferPayload.Direction s = OfferPayload.Direction.values()[i]; + for (int i = 0; i < OfferDirection.values().length; i++) { + OfferDirection s = OfferDirection.values()[i]; sb.append(" "); sb.append(s.toString()); sb.append(" = "); diff --git a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java index 88f5efeb5f..5139be0f9e 100644 --- a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java +++ b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java @@ -24,8 +24,8 @@ import bisq.core.offer.OpenOfferManager; import bisq.core.support.dispute.arbitration.ArbitrationDisputeListService; import bisq.core.support.dispute.mediation.MediationDisputeListService; import bisq.core.support.dispute.refund.RefundDisputeListService; +import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.TradeManager; -import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.failed.FailedTradesManager; import bisq.core.user.Preferences; import bisq.core.user.User; diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index 019cc898d4..061ac9f3c0 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -35,11 +35,11 @@ import bisq.core.support.dispute.messages.DisputeResultMessage; import bisq.core.support.dispute.messages.OpenNewDisputeMessage; import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; import bisq.core.support.messages.ChatMessage; +import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.trade.TradeDataValidation; import bisq.core.trade.TradeManager; -import bisq.core.trade.closed.ClosedTradableManager; import bisq.network.p2p.BootstrapListener; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; @@ -290,6 +290,15 @@ public abstract class DisputeManager> extends Sup return pubKeyRing.equals(dispute.getTraderPubKeyRing()); } + public Optional findOwnDispute(String tradeId) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return Optional.empty(); + } + return disputeList.stream().filter(e -> e.getTradeId().equals(tradeId)).findAny(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Message handler /////////////////////////////////////////////////////////////////////////////////////////// @@ -941,11 +950,11 @@ public abstract class DisputeManager> extends Sup } String percentagePriceDetails = offerPayload.isUseMarketBasedPrice() ? - " (market based price was used: " + offerPayload.getMarketPriceMargin() * 100 + "%)" : + " (market based price was used: " + offerPayload.getMarketPriceMarginPct() * 100 + "%)" : " (fix price was used)"; String priceInfoText = "System message: " + headline + - "\n\nTrade price: " + contract.getTradePrice().toFriendlyString() + percentagePriceDetails + + "\n\nTrade price: " + contract.getPrice().toFriendlyString() + percentagePriceDetails + "\nTrade amount: " + tradeAmount.toFriendlyString() + "\nPrice at dispute opening: " + priceAtDisputeOpening.toFriendlyString() + optionTradeDetails; diff --git a/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentLookupMap.java b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentLookupMap.java index 9d9cdcfea8..ff103f012b 100644 --- a/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentLookupMap.java +++ b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentLookupMap.java @@ -28,9 +28,12 @@ public class DisputeAgentLookupMap { // See also: https://bisq.wiki/Finding_your_mediator @Nullable - public static String getKeyBaseUserName(String fullAddress) { + public static String getMatrixUserName(String fullAddress) { + if (fullAddress.matches("localhost(.*)")) { + return fullAddress; // on regtest, agent displays as localhost + } switch (fullAddress) { - case "sjlho4zwp3gecspf.onion:9999": + case "7hkpotiyaukuzcfy6faihjaols5r2mkysz7bm3wrhhbpbphzz3zbwyqd.onion:9999": return "leo816"; case "wizbisqzd7ku25di7p2ztsajioabihlnyp5lq5av66tmu7do2dke2tid.onion:9999": return "wiz"; @@ -40,9 +43,19 @@ public class DisputeAgentLookupMap { return "huey735"; case "3z5jnirlccgxzoxc6zwkcgwj66bugvqplzf6z2iyd5oxifiaorhnanqd.onion:9999": return "refundagent2"; + case "6c4cim7h7t3bm4bnchbf727qrhdfrfr6lhod25wjtizm2sifpkktvwad.onion:9999": + return "pazza83"; default: log.warn("No user name for dispute agent with address {} found.", fullAddress); return Res.get("shared.na"); } } + + public static String getMatrixLinkForAgent(String onion) { + // when a new mediator starts or an onion address changes, mediator name won't be known until + // the table above is updated in the software. + // as a stopgap measure, replace unknown ones with a link to the Bisq team + String agentName = getMatrixUserName(onion).replaceAll(Res.get("shared.na"), "bisq"); + return "https://matrix.to/#/@" + agentName + ":matrix.org"; + } } diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java index 738173783e..08e48564af 100644 --- a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java @@ -38,11 +38,11 @@ import bisq.core.support.dispute.messages.OpenNewDisputeMessage; import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.SupportMessage; +import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.Contract; import bisq.core.trade.Tradable; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; -import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.util.ParsingUtils; import bisq.network.p2p.AckMessageSourceType; @@ -63,7 +63,6 @@ import com.google.inject.Singleton; import java.math.BigInteger; -import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -498,6 +497,7 @@ public final class ArbitrationManager extends DisputeManager disputeOptional = findDispute(tradeId); if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId); Dispute dispute = disputeOptional.get(); diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java index 06575394a1..746788639e 100644 --- a/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java @@ -34,9 +34,9 @@ import bisq.core.support.dispute.messages.OpenNewDisputeMessage; import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.SupportMessage; +import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; -import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.protocol.DisputeProtocol; import bisq.core.trade.protocol.ProcessModel; diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java index 401ce5d40d..aafd088e16 100644 --- a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -34,10 +34,9 @@ import bisq.core.support.dispute.messages.OpenNewDisputeMessage; import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.SupportMessage; +import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; -import bisq.core.trade.closed.ClosedTradableManager; - import bisq.network.p2p.AckMessageSourceType; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; diff --git a/core/src/main/java/bisq/core/trade/ArbitratorTrade.java b/core/src/main/java/bisq/core/trade/ArbitratorTrade.java index 79e51805ce..3f67d45c5a 100644 --- a/core/src/main/java/bisq/core/trade/ArbitratorTrade.java +++ b/core/src/main/java/bisq/core/trade/ArbitratorTrade.java @@ -62,9 +62,9 @@ public class ArbitratorTrade extends Trade { } return fromProto(new ArbitratorTrade( Offer.fromProto(proto.getOffer()), - Coin.valueOf(proto.getTradeAmountAsLong()), + Coin.valueOf(proto.getAmountAsLong()), Coin.valueOf(proto.getTakerFeeAsLong()), - proto.getTradePrice(), + proto.getPrice(), xmrWalletService, processModel, uid, diff --git a/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java b/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java index 2d7dbda087..e1fc7ed1ad 100644 --- a/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java +++ b/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java @@ -86,9 +86,9 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { } BuyerAsMakerTrade trade = new BuyerAsMakerTrade( Offer.fromProto(proto.getOffer()), - Coin.valueOf(proto.getTradeAmountAsLong()), + Coin.valueOf(proto.getAmountAsLong()), Coin.valueOf(proto.getTakerFeeAsLong()), - proto.getTradePrice(), + proto.getPrice(), xmrWalletService, processModel, uid, @@ -96,8 +96,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { proto.hasTakerNodeAddress() ? NodeAddress.fromProto(proto.getTakerNodeAddress()) : null, proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null); - trade.setTradeAmountAsLong(proto.getTradeAmountAsLong()); - trade.setTradePrice(proto.getTradePrice()); + trade.setAmountAsLong(proto.getAmountAsLong()); + trade.setPrice(proto.getPrice()); trade.setMakerNodeAddress(proto.hasMakerNodeAddress() ? NodeAddress.fromProto(proto.getMakerNodeAddress()) : null); trade.setTakerNodeAddress(proto.hasTakerNodeAddress() ? NodeAddress.fromProto(proto.getTakerNodeAddress()) : null); diff --git a/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java b/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java index a868c9a643..c984711c48 100644 --- a/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java +++ b/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java @@ -87,9 +87,9 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade { } return fromProto(new BuyerAsTakerTrade( Offer.fromProto(proto.getOffer()), - Coin.valueOf(proto.getTradeAmountAsLong()), + Coin.valueOf(proto.getAmountAsLong()), Coin.valueOf(proto.getTakerFeeAsLong()), - proto.getTradePrice(), + proto.getPrice(), xmrWalletService, processModel, uid, diff --git a/core/src/main/java/bisq/core/trade/BuyerTrade.java b/core/src/main/java/bisq/core/trade/BuyerTrade.java index abd23c7cd9..b01c602266 100644 --- a/core/src/main/java/bisq/core/trade/BuyerTrade.java +++ b/core/src/main/java/bisq/core/trade/BuyerTrade.java @@ -57,8 +57,8 @@ public abstract class BuyerTrade extends Trade { @Override public Coin getPayoutAmount() { - checkNotNull(getTradeAmount(), "Invalid state: getTradeAmount() = null"); - return checkNotNull(getOffer()).getBuyerSecurityDeposit().add(getTradeAmount()); + checkNotNull(getAmount(), "Invalid state: getTradeAmount() = null"); + return checkNotNull(getOffer()).getBuyerSecurityDeposit().add(getAmount()); } @Override diff --git a/core/src/main/java/bisq/core/trade/closed/CleanupMailboxMessages.java b/core/src/main/java/bisq/core/trade/CleanupMailboxMessages.java similarity index 98% rename from core/src/main/java/bisq/core/trade/closed/CleanupMailboxMessages.java rename to core/src/main/java/bisq/core/trade/CleanupMailboxMessages.java index d86ecd6054..dcd66d05c0 100644 --- a/core/src/main/java/bisq/core/trade/closed/CleanupMailboxMessages.java +++ b/core/src/main/java/bisq/core/trade/CleanupMailboxMessages.java @@ -15,9 +15,8 @@ * along with Haveno. If not, see . */ -package bisq.core.trade.closed; +package bisq.core.trade; -import bisq.core.trade.Trade; import bisq.core.trade.messages.TradeMessage; import bisq.network.p2p.AckMessage; diff --git a/core/src/main/java/bisq/core/trade/CleanupMailboxMessagesService.java b/core/src/main/java/bisq/core/trade/CleanupMailboxMessagesService.java new file mode 100644 index 0000000000..6b05b82385 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/CleanupMailboxMessagesService.java @@ -0,0 +1,134 @@ +/* + * 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 . + */ + +package bisq.core.trade; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.TradeMessage; +import bisq.network.p2p.AckMessage; +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.DecryptedMessageWithPubKey; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.mailbox.MailboxMessage; +import bisq.network.p2p.mailbox.MailboxMessageService; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.network.NetworkEnvelope; + +import javax.inject.Inject; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +//TODO with the redesign of mailbox messages that is not required anymore. We leave it for now as we want to minimize +// changes for the 1.5.0 release but we should clean up afterwards... + +/** + * Util for removing pending mailbox messages in case the trade has been closed by the seller after confirming receipt + * and a AckMessage as mailbox message will be sent by the buyer once they go online. In that case the seller's trade + * is closed already and the TradeProtocol is not executing the message processing, thus the mailbox message would not + * be removed. To ensure that in such cases (as well other potential cases in failure scenarios) the mailbox message + * gets removed from the network we use that util. + * + * This class must not be injected as a singleton! + */ +@Slf4j +public class CleanupMailboxMessagesService { + private final P2PService p2PService; + private final MailboxMessageService mailboxMessageService; + + @Inject + public CleanupMailboxMessagesService(P2PService p2PService, MailboxMessageService mailboxMessageService) { + this.p2PService = p2PService; + this.mailboxMessageService = mailboxMessageService; + } + + public void handleTrades(List trades) { + // We wrap in a try catch as in failed trades we cannot be sure if expected data is set, so we could get + // a NullPointer and do not want that this escalate to the user. + try { + if (p2PService.isBootstrapped()) { + cleanupMailboxMessages(trades); + } else { + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + cleanupMailboxMessages(trades); + } + }); + } + } catch (Throwable t) { + log.error("Cleanup mailbox messages failed. {}", t.toString()); + } + } + + private void cleanupMailboxMessages(List trades) { + mailboxMessageService.getMyDecryptedMailboxMessages() + .forEach(message -> handleDecryptedMessageWithPubKey(message, trades)); + } + + private void handleDecryptedMessageWithPubKey(DecryptedMessageWithPubKey decryptedMessageWithPubKey, + List trades) { + trades.stream() + .filter(trade -> isMessageForTrade(decryptedMessageWithPubKey, trade)) + .filter(trade -> isPubKeyValid(decryptedMessageWithPubKey, trade)) + .filter(trade -> decryptedMessageWithPubKey.getNetworkEnvelope() instanceof MailboxMessage) + .forEach(trade -> removeEntryFromMailbox((MailboxMessage) decryptedMessageWithPubKey.getNetworkEnvelope(), trade)); + } + + private boolean isMessageForTrade(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Trade trade) { + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + if (networkEnvelope instanceof TradeMessage) { + return isMyMessage((TradeMessage) networkEnvelope, trade); + } else if (networkEnvelope instanceof AckMessage) { + return isMyMessage((AckMessage) networkEnvelope, trade); + } + // Instance must be TradeMessage or AckMessage. + return false; + } + + private void removeEntryFromMailbox(MailboxMessage mailboxMessage, Trade trade) { + log.info("We found a pending mailbox message ({}) for trade {}. " + + "As the trade is closed we remove the mailbox message.", + mailboxMessage.getClass().getSimpleName(), trade.getId()); + mailboxMessageService.removeMailboxMsg(mailboxMessage); + } + + private boolean isMyMessage(TradeMessage message, Trade trade) { + return message.getTradeId().equals(trade.getId()); + } + + private boolean isMyMessage(AckMessage ackMessage, Trade trade) { + return ackMessage.getSourceType() == AckMessageSourceType.TRADE_MESSAGE && + ackMessage.getSourceId().equals(trade.getId()); + } + + private boolean isPubKeyValid(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Trade trade) { + // We can only validate the peers pubKey if we have it already. If we are the taker we get it from the offer + // Otherwise it depends on the state of the trade protocol if we have received the peers pubKeyRing already. + PubKeyRing peersPubKeyRing = trade.getTradingPeer().getPubKeyRing(); + boolean isValid = true; + if (peersPubKeyRing != null && + !decryptedMessageWithPubKey.getSignaturePubKey().equals(peersPubKeyRing.getSignaturePubKey())) { + isValid = false; + log.warn("SignaturePubKey in decryptedMessageWithPubKey does not match the SignaturePubKey we have set for our trading peer."); + } + return isValid; + } +} diff --git a/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java b/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java new file mode 100644 index 0000000000..5b0796d43b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java @@ -0,0 +1,194 @@ +/* + * 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 . + */ + +package bisq.core.trade; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Volume; +import bisq.core.offer.OpenOffer; +import bisq.core.trade.Tradable; +import bisq.core.trade.Trade; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.CoinFormatter; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Monetary; +import org.bitcoinj.core.TransactionConfidence; +import org.bitcoinj.utils.Fiat; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.trade.ClosedTradableUtil.*; +import static bisq.core.trade.Trade.DisputeState.DISPUTE_CLOSED; +import static bisq.core.trade.Trade.DisputeState.MEDIATION_CLOSED; +import static bisq.core.trade.Trade.DisputeState.REFUND_REQUEST_CLOSED; +import static bisq.core.util.FormattingUtils.BTC_FORMATTER_KEY; +import static bisq.core.util.FormattingUtils.formatPercentagePrice; +import static bisq.core.util.FormattingUtils.formatToPercentWithSymbol; +import static bisq.core.util.VolumeUtil.formatVolume; +import static bisq.core.util.VolumeUtil.formatVolumeWithCode; +import static org.bitcoinj.core.TransactionConfidence.ConfidenceType.BUILDING; +import static org.bitcoinj.core.TransactionConfidence.ConfidenceType.PENDING; + +@Slf4j +@Singleton +public class ClosedTradableFormatter { + // Resource bundle i18n keys with Desktop UI specific property names, + // having "generic-enough" property values to be referenced in the core layer. + private static final String I18N_KEY_TOTAL_AMOUNT = "closedTradesSummaryWindow.totalAmount.value"; + private static final String I18N_KEY_TOTAL_TX_FEE = "closedTradesSummaryWindow.totalMinerFee.value"; + private static final String I18N_KEY_TOTAL_TRADE_FEE_BTC = "closedTradesSummaryWindow.totalTradeFeeInBtc.value"; + private static final String I18N_KEY_TOTAL_TRADE_FEE_BSQ = "closedTradesSummaryWindow.totalTradeFeeInBsq.value"; + + private final CoinFormatter btcFormatter; + private final ClosedTradableManager closedTradableManager; + + @Inject + public ClosedTradableFormatter(ClosedTradableManager closedTradableManager, + @Named(BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { + this.closedTradableManager = closedTradableManager; + this.btcFormatter = btcFormatter; + } + + public String getAmountAsString(Tradable tradable) { + return tradable.getOptionalAmount().map(btcFormatter::formatCoin).orElse(""); + } + + public String getTotalAmountWithVolumeAsString(Coin totalTradeAmount, Volume volume) { + return Res.get(I18N_KEY_TOTAL_AMOUNT, + btcFormatter.formatCoin(totalTradeAmount, true), + formatVolumeWithCode(volume)); + } + + public String getTxFeeAsString(Tradable tradable) { + return btcFormatter.formatCoin(getTxFee(tradable)); + } + + public String getTotalTxFeeAsString(Coin totalTradeAmount, Coin totalTxFee) { + double percentage = ((double) totalTxFee.value) / totalTradeAmount.value; + return Res.get(I18N_KEY_TOTAL_TX_FEE, + btcFormatter.formatCoin(totalTxFee, true), + formatToPercentWithSymbol(percentage)); + } + + public String getBuyerSecurityDepositAsString(Tradable tradable) { + return btcFormatter.formatCoin(tradable.getOffer().getBuyerSecurityDeposit()); + } + + public String getSellerSecurityDepositAsString(Tradable tradable) { + return btcFormatter.formatCoin(tradable.getOffer().getSellerSecurityDeposit()); + } + + public String getTradeFeeAsString(Tradable tradable, boolean appendCode) { + closedTradableManager.getBtcTradeFee(tradable); + return btcFormatter.formatCoin(Coin.valueOf(closedTradableManager.getBtcTradeFee(tradable)), appendCode); + } + + public String getTotalTradeFeeInBtcAsString(Coin totalTradeAmount, Coin totalTradeFee) { + double percentage = ((double) totalTradeFee.value) / totalTradeAmount.value; + return Res.get(I18N_KEY_TOTAL_TRADE_FEE_BTC, + btcFormatter.formatCoin(totalTradeFee, true), + formatToPercentWithSymbol(percentage)); + } + + public String getPriceDeviationAsString(Tradable tradable) { + if (tradable.getOffer().isUseMarketBasedPrice()) { + return formatPercentagePrice(tradable.getOffer().getMarketPriceMarginPct()); + } else { + return Res.get("shared.na"); + } + } + + public String getVolumeAsString(Tradable tradable, boolean appendCode) { + return tradable.getOptionalVolume().map(volume -> formatVolume(volume, appendCode)).orElse(""); + } + + public String getVolumeCurrencyAsString(Tradable tradable) { + return tradable.getOptionalVolume().map(Volume::getCurrencyCode).orElse(""); + } + + public String getPriceAsString(Tradable tradable) { + return tradable.getOptionalPrice().map(FormattingUtils::formatPrice).orElse(""); + } + + public Map getTotalVolumeByCurrencyAsString(List tradableList) { + return getTotalVolumeByCurrency(tradableList).entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> { + String currencyCode = entry.getKey(); + Monetary monetary; + if (CurrencyUtil.isCryptoCurrency(currencyCode)) { + monetary = Altcoin.valueOf(currencyCode, entry.getValue()); + } else { + monetary = Fiat.valueOf(currencyCode, entry.getValue()); + } + return formatVolumeWithCode(new Volume(monetary)); + } + )); + } + + public String getStateAsString(Tradable tradable) { + if (tradable == null) { + return ""; + } + + if (isBisqV1Trade(tradable)) { + Trade trade = castToTrade(tradable); + if (trade.isWithdrawn() || trade.isPayoutPublished()) { + return Res.get("portfolio.closed.completed"); + } else if (trade.getDisputeState() == DISPUTE_CLOSED) { + return Res.get("portfolio.closed.ticketClosed"); + } else if (trade.getDisputeState() == MEDIATION_CLOSED) { + return Res.get("portfolio.closed.mediationTicketClosed"); + } else if (trade.getDisputeState() == REFUND_REQUEST_CLOSED) { + return Res.get("portfolio.closed.ticketClosed"); + } else { + log.error("That must not happen. We got a pending state but we are in" + + " the closed trades list. state={}", + trade.getState().name()); + return Res.get("shared.na"); + } + } else if (isOpenOffer(tradable)) { + OpenOffer.State state = ((OpenOffer) tradable).getState(); + log.trace("OpenOffer state={}", state); + switch (state) { + case AVAILABLE: + case RESERVED: + case CLOSED: + case DEACTIVATED: + log.error("Invalid state {}", state); + return state.name(); + case CANCELED: + return Res.get("portfolio.closed.canceled"); + default: + log.error("Unhandled state {}", state); + return state.name(); + } + } + return Res.get("shared.na"); + } +} diff --git a/core/src/main/java/bisq/core/trade/ClosedTradableManager.java b/core/src/main/java/bisq/core/trade/ClosedTradableManager.java new file mode 100644 index 0000000000..3eed4b4c7a --- /dev/null +++ b/core/src/main/java/bisq/core/trade/ClosedTradableManager.java @@ -0,0 +1,232 @@ +/* + * 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 . + */ + +package bisq.core.trade; + +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; +import bisq.core.offer.Offer; +import bisq.core.offer.OpenOffer; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.Preferences; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.KeyRing; +import bisq.common.persistence.PersistenceManager; +import bisq.common.proto.persistable.PersistedDataHost; +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.Fiat; + +import com.google.inject.Inject; + +import com.google.common.collect.ImmutableList; + +import javafx.collections.ObservableList; + +import java.time.Instant; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.offer.OpenOffer.State.CANCELED; +import static bisq.core.trade.ClosedTradableUtil.castToTradeModel; +import static bisq.core.trade.ClosedTradableUtil.isOpenOffer; +import static bisq.core.util.AveragePriceUtil.getAveragePriceTuple; + +/** + * Manages closed trades or offers. + * BsqSwap trades are once confirmed moved in the closed trades domain as well. + * We do not manage the persistence of BsqSwap trades here but in BsqSwapTradeManager. + */ +@Slf4j +public class ClosedTradableManager implements PersistedDataHost { + private final KeyRing keyRing; + private final PriceFeedService priceFeedService; + private final Preferences preferences; + private final TradeStatisticsManager tradeStatisticsManager; + private final PersistenceManager> persistenceManager; + private final CleanupMailboxMessagesService cleanupMailboxMessagesService; + private final DumpDelayedPayoutTx dumpDelayedPayoutTx; + + private final TradableList closedTradables = new TradableList<>(); + + @Inject + public ClosedTradableManager(KeyRing keyRing, + PriceFeedService priceFeedService, + Preferences preferences, + TradeStatisticsManager tradeStatisticsManager, + PersistenceManager> persistenceManager, + CleanupMailboxMessagesService cleanupMailboxMessagesService, + DumpDelayedPayoutTx dumpDelayedPayoutTx) { + this.keyRing = keyRing; + this.priceFeedService = priceFeedService; + this.preferences = preferences; + this.tradeStatisticsManager = tradeStatisticsManager; + this.cleanupMailboxMessagesService = cleanupMailboxMessagesService; + this.dumpDelayedPayoutTx = dumpDelayedPayoutTx; + this.persistenceManager = persistenceManager; + + this.persistenceManager.initialize(closedTradables, "ClosedTrades", PersistenceManager.Source.PRIVATE); + } + + @Override + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted(persisted -> { + closedTradables.setAll(persisted.getList()); + closedTradables.stream() + .filter(tradable -> tradable.getOffer() != null) + .forEach(tradable -> tradable.getOffer().setPriceFeedService(priceFeedService)); + dumpDelayedPayoutTx.maybeDumpDelayedPayoutTxs(closedTradables, "delayed_payout_txs_closed"); + completeHandler.run(); + }, + completeHandler); + } + + public void onAllServicesInitialized() { + cleanupMailboxMessagesService.handleTrades(getClosedTrades()); + maybeClearSensitiveData(); + } + + public void add(Tradable tradable) { + if (closedTradables.add(tradable)) { + maybeClearSensitiveData(); + requestPersistence(); + } + } + + public void remove(Tradable tradable) { + if (closedTradables.remove(tradable)) { + requestPersistence(); + } + } + + public boolean wasMyOffer(Offer offer) { + return offer.isMyOffer(keyRing); + } + + public ObservableList getObservableList() { + return closedTradables.getObservableList(); + } + + public List getTradableList() { + return ImmutableList.copyOf(new ArrayList<>(getObservableList())); + } + + public List getClosedTrades() { + return ImmutableList.copyOf(getObservableList().stream() + .filter(e -> e instanceof Trade) + .map(e -> (Trade) e) + .collect(Collectors.toList())); + } + + public List getCanceledOpenOffers() { + return ImmutableList.copyOf(getObservableList().stream() + .filter(e -> (e instanceof OpenOffer) && ((OpenOffer) e).getState().equals(CANCELED)) + .map(e -> (OpenOffer) e) + .collect(Collectors.toList())); + } + + public Optional getTradableById(String id) { + return closedTradables.stream().filter(e -> e.getId().equals(id)).findFirst(); + } + + public void maybeClearSensitiveData() { + log.info("checking closed trades eligibility for having sensitive data cleared"); + closedTradables.stream() + .filter(e -> e instanceof Trade) + .map(e -> (Trade) e) + .filter(e -> canTradeHaveSensitiveDataCleared(e.getId())) + .forEach(Trade::maybeClearSensitiveData); + requestPersistence(); + } + + public boolean canTradeHaveSensitiveDataCleared(String tradeId) { + Instant safeDate = getSafeDateForSensitiveDataClearing(); + return closedTradables.stream() + .filter(e -> e.getId().equals(tradeId)) + .filter(e -> e.getDate().toInstant().isBefore(safeDate)) + .count() > 0; + } + + public Instant getSafeDateForSensitiveDataClearing() { + return Instant.ofEpochSecond(Instant.now().getEpochSecond() + - TimeUnit.DAYS.toSeconds(preferences.getClearDataAfterDays())); + } + + public Stream getTradesStreamWithFundsLockedIn() { + return getClosedTrades().stream() + .filter(Trade::isFundsLockedIn); + } + + public Stream getTradeModelStream() { + return getClosedTrades().stream(); + } + + public int getNumPastTrades(Tradable tradable) { + if (isOpenOffer(tradable)) { + return 0; + } + NodeAddress addressInTrade = castToTradeModel(tradable).getTradingPeerNodeAddress(); + return (int) getTradeModelStream() + .map(Trade::getTradingPeerNodeAddress) + .filter(Objects::nonNull) + .filter(address -> address.equals(addressInTrade)) + .count(); + } + + public Coin getTotalTradeFee(List tradableList) { + return Coin.valueOf(tradableList.stream() + .mapToLong(tradable -> getTradeFee(tradable)) + .sum()); + } + + private long getTradeFee(Tradable tradable) { + return getBtcTradeFee(tradable); + } + + public long getBtcTradeFee(Tradable tradable) { + return isMaker(tradable) ? + tradable.getOptionalMakerFee().orElse(Coin.ZERO).value : + tradable.getOptionalTakerFee().orElse(Coin.ZERO).value; + } + + public boolean isMaker(Tradable tradable) { + return tradable instanceof MakerTrade || tradable.getOffer().isMyOffer(keyRing); + } + + public Volume getBsqVolumeInUsdWithAveragePrice(Coin amount) { + Tuple2 tuple = getAveragePriceTuple(preferences, tradeStatisticsManager, 30); + Price usdPrice = tuple.first; + long value = Math.round(amount.value * usdPrice.getValue() / 100d); + return new Volume(Fiat.valueOf("USD", value)); + } + + private void requestPersistence() { + persistenceManager.requestPersistence(); + } +} diff --git a/core/src/main/java/bisq/core/trade/ClosedTradableUtil.java b/core/src/main/java/bisq/core/trade/ClosedTradableUtil.java new file mode 100644 index 0000000000..dc60ab0ab0 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/ClosedTradableUtil.java @@ -0,0 +1,75 @@ +/* + * 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 . + */ + +package bisq.core.trade; + +import bisq.core.offer.OpenOffer; +import bisq.core.trade.Tradable; +import bisq.core.trade.Trade; + +import org.bitcoinj.core.Coin; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ClosedTradableUtil { + public static Coin getTotalAmount(List tradableList) { + return Coin.valueOf(tradableList.stream() + .flatMap(tradable -> tradable.getOptionalAmountAsLong().stream()) + .mapToLong(value -> value) + .sum()); + } + + public static Coin getTotalTxFee(List tradableList) { + return Coin.valueOf(tradableList.stream() + .mapToLong(tradable -> getTxFee(tradable).getValue()) + .sum()); + } + + public static Map getTotalVolumeByCurrency(List tradableList) { + Map map = new HashMap<>(); + tradableList.stream() + .flatMap(tradable -> tradable.getOptionalVolume().stream()) + .forEach(volume -> { + String currencyCode = volume.getCurrencyCode(); + map.putIfAbsent(currencyCode, 0L); + map.put(currencyCode, volume.getValue() + map.get(currencyCode)); + }); + return map; + } + + public static Coin getTxFee(Tradable tradable) { + return tradable.getOptionalTxFee().orElse(Coin.ZERO); + } + + public static boolean isOpenOffer(Tradable tradable) { + return tradable instanceof OpenOffer; + } + + public static boolean isBisqV1Trade(Tradable tradable) { + return tradable instanceof Trade; + } + + public static Trade castToTrade(Tradable tradable) { + return (Trade) tradable; + } + + public static Trade castToTradeModel(Tradable tradable) { + return (Trade) tradable; + } +} diff --git a/core/src/main/java/bisq/core/trade/Contract.java b/core/src/main/java/bisq/core/trade/Contract.java index c42daf0319..e11741651a 100644 --- a/core/src/main/java/bisq/core/trade/Contract.java +++ b/core/src/main/java/bisq/core/trade/Contract.java @@ -23,6 +23,7 @@ import bisq.core.monetary.Volume; import bisq.core.offer.OfferPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.proto.CoreProtoResolver; +import bisq.core.util.JsonUtil; import bisq.core.util.VolumeUtil; import bisq.network.p2p.NodeAddress; @@ -30,7 +31,6 @@ import com.google.protobuf.ByteString; import bisq.common.crypto.PubKeyRing; import bisq.common.proto.network.NetworkPayload; import bisq.common.util.JsonExclude; -import bisq.common.util.Utilities; import org.bitcoinj.core.Coin; @@ -213,7 +213,7 @@ public final class Contract implements NetworkPayload { } public Volume getTradeVolume() { - Volume volumeByAmount = getTradePrice().getVolumeByAmount(getTradeAmount()); + Volume volumeByAmount = getPrice().getVolumeByAmount(getTradeAmount()); if (getPaymentMethodId().equals(PaymentMethod.HAL_CASH_ID)) volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); @@ -223,7 +223,7 @@ public final class Contract implements NetworkPayload { return volumeByAmount; } - public Price getTradePrice() { + public Price getPrice() { return Price.valueOf(offerPayload.getCurrencyCode(), tradePrice); } @@ -257,7 +257,7 @@ public final class Contract implements NetworkPayload { } public void printDiff(@Nullable String peersContractAsJson) { - String json = Utilities.objectToJson(this); + String json = JsonUtil.objectToJson(this); String diff = StringUtils.difference(json, peersContractAsJson); if (!diff.isEmpty()) { log.warn("Diff of both contracts: \n" + diff); diff --git a/core/src/main/java/bisq/core/trade/DumpDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/DumpDelayedPayoutTx.java index 2334f72e7a..8d22f83712 100644 --- a/core/src/main/java/bisq/core/trade/DumpDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/DumpDelayedPayoutTx.java @@ -20,7 +20,7 @@ package bisq.core.trade; import bisq.common.config.Config; import bisq.common.file.JsonFileManager; import bisq.common.util.Utilities; - +import bisq.core.util.JsonUtil; import javax.inject.Inject; import javax.inject.Named; @@ -58,7 +58,7 @@ public class DumpDelayedPayoutTx { .map(trade -> new DelayedPayoutHash(trade.getId(), Utilities.bytesAsHexString(((Trade) trade).getDelayedPayoutTxBytes()))) .collect(Collectors.toList()); - jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(delayedPayoutHashes), fileName); + jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(delayedPayoutHashes), fileName); } } diff --git a/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java b/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java index b41d0a288a..7484b5751e 100644 --- a/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java +++ b/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java @@ -87,9 +87,9 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade } SellerAsMakerTrade trade = new SellerAsMakerTrade( Offer.fromProto(proto.getOffer()), - Coin.valueOf(proto.getTradeAmountAsLong()), + Coin.valueOf(proto.getAmountAsLong()), Coin.valueOf(proto.getTakerFeeAsLong()), - proto.getTradePrice(), + proto.getPrice(), xmrWalletService, processModel, uid, @@ -97,8 +97,8 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade proto.hasTakerNodeAddress() ? NodeAddress.fromProto(proto.getTakerNodeAddress()) : null, proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null); - trade.setTradeAmountAsLong(proto.getTradeAmountAsLong()); - trade.setTradePrice(proto.getTradePrice()); + trade.setAmountAsLong(proto.getAmountAsLong()); + trade.setPrice(proto.getPrice()); return fromProto(trade, proto, diff --git a/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java b/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java index a2ae17707c..7b94f3dd46 100644 --- a/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java +++ b/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java @@ -87,9 +87,9 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade } return fromProto(new SellerAsTakerTrade( Offer.fromProto(proto.getOffer()), - Coin.valueOf(proto.getTradeAmountAsLong()), + Coin.valueOf(proto.getAmountAsLong()), Coin.valueOf(proto.getTakerFeeAsLong()), - proto.getTradePrice(), + proto.getPrice(), xmrWalletService, processModel, uid, diff --git a/core/src/main/java/bisq/core/trade/Tradable.java b/core/src/main/java/bisq/core/trade/Tradable.java index bd5adf9eaa..9ac50f370b 100644 --- a/core/src/main/java/bisq/core/trade/Tradable.java +++ b/core/src/main/java/bisq/core/trade/Tradable.java @@ -17,11 +17,18 @@ package bisq.core.trade; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; import bisq.core.offer.Offer; +import bisq.network.p2p.NodeAddress; + import bisq.common.proto.persistable.PersistablePayload; +import org.bitcoinj.core.Coin; + import java.util.Date; +import java.util.Optional; public interface Tradable extends PersistablePayload { Offer getOffer(); @@ -31,4 +38,44 @@ public interface Tradable extends PersistablePayload { String getId(); String getShortId(); + + default Optional asTradeModel() { + if (this instanceof Trade) { + return Optional.of(((Trade) this)); + } else { + return Optional.empty(); + } + } + + default Optional getOptionalVolume() { + return asTradeModel().map(Trade::getVolume).or(() -> Optional.ofNullable(getOffer().getVolume())); + } + + default Optional getOptionalPrice() { + return asTradeModel().map(Trade::getPrice).or(() -> Optional.ofNullable(getOffer().getPrice())); + } + + default Optional getOptionalAmount() { + return asTradeModel().map(Trade::getAmount); + } + + default Optional getOptionalAmountAsLong() { + return asTradeModel().map(Trade::getAmountAsLong); + } + + default Optional getOptionalTxFee() { + return asTradeModel().map(Trade::getTxFee); + } + + default Optional getOptionalTakerFee() { + return asTradeModel().map(Trade::getTakerFee); + } + + default Optional getOptionalMakerFee() { + return asTradeModel().map(Trade::getOffer).map(Offer::getMakerFee).or(() -> Optional.ofNullable(getOffer().getMakerFee())); + } + + default Optional getOptionalTradingPeerNodeAddress() { + return asTradeModel().map(Trade::getTradingPeerNodeAddress); + } } diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index 6f9281333b..3ed4a20e1c 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -23,7 +23,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload.Direction; +import bisq.core.offer.OfferDirection; import bisq.core.payment.payload.PaymentMethod; import bisq.core.proto.CoreProtoResolver; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; @@ -335,16 +335,16 @@ public abstract class Trade implements Tradable, Model { private String payoutTxId; @Getter @Setter - private long tradeAmountAsLong; + private long amountAsLong; @Setter - private long tradePrice; + private long price; @Nullable @Getter private State state = State.PREPARATION; @Getter private DisputeState disputeState = DisputeState.NO_DISPUTE; @Getter - private TradePeriodState tradePeriodState = TradePeriodState.FIRST_HALF; + private TradePeriodState periodState = TradePeriodState.FIRST_HALF; @Nullable @Getter @Setter @@ -393,9 +393,9 @@ public abstract class Trade implements Tradable, Model { transient final private ObjectProperty stateProperty = new SimpleObjectProperty<>(state); transient final private ObjectProperty statePhaseProperty = new SimpleObjectProperty<>(state.phase); transient final private ObjectProperty disputeStateProperty = new SimpleObjectProperty<>(disputeState); - transient final private ObjectProperty tradePeriodStateProperty = new SimpleObjectProperty<>(tradePeriodState); + transient final private ObjectProperty tradePeriodStateProperty = new SimpleObjectProperty<>(periodState); transient final private StringProperty errorMessageProperty = new SimpleStringProperty(); - + // Mutable @Getter transient private boolean isInitialized; @@ -497,7 +497,7 @@ public abstract class Trade implements Tradable, Model { this.tradeAmount = tradeAmount; this.txFee = Coin.valueOf(0); // TODO (woodser): remove this field this.takerFee = takerFee; - this.tradePrice = tradePrice; + this.price = tradePrice; this.xmrWalletService = xmrWalletService; this.processModel = processModel; this.uid = uid; @@ -511,7 +511,7 @@ public abstract class Trade implements Tradable, Model { this.takerNodeAddress = takerNodeAddress; this.arbitratorNodeAddress = arbitratorNodeAddress; - setTradeAmount(tradeAmount); + setAmount(tradeAmount); } @@ -570,7 +570,7 @@ public abstract class Trade implements Tradable, Model { takerNodeAddress, arbitratorNodeAddress); - setTradeAmount(tradeAmount); + setAmount(tradeAmount); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -585,11 +585,11 @@ public abstract class Trade implements Tradable, Model { .setTakerFeeAsLong(takerFeeAsLong) .setTakeOfferDate(takeOfferDate) .setProcessModel(processModel.toProtoMessage()) - .setTradeAmountAsLong(tradeAmountAsLong) - .setTradePrice(tradePrice) + .setAmountAsLong(amountAsLong) + .setPrice(price) .setState(Trade.State.toProtoMessage(state)) .setDisputeState(Trade.DisputeState.toProtoMessage(disputeState)) - .setTradePeriodState(Trade.TradePeriodState.toProtoMessage(tradePeriodState)) + .setPeriodState(Trade.TradePeriodState.toProtoMessage(periodState)) .addAllChatMessage(chatMessages.stream() .map(msg -> msg.toProtoNetworkEnvelope().getChatMessage()) .collect(Collectors.toList())) @@ -625,7 +625,7 @@ public abstract class Trade implements Tradable, Model { trade.setTakeOfferDate(proto.getTakeOfferDate()); trade.setState(State.fromProto(proto.getState())); trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState())); - trade.setTradePeriodState(TradePeriodState.fromProto(proto.getTradePeriodState())); + trade.setPeriodState(TradePeriodState.fromProto(proto.getPeriodState())); trade.setTakerFeeTxId(ProtoUtil.stringOrNullFromProto(proto.getTakerFeeTxId())); trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId())); trade.setContract(proto.hasContract() ? Contract.fromProto(proto.getContract(), coreProtoResolver) : null); @@ -715,11 +715,11 @@ public abstract class Trade implements Tradable, Model { * @return the contract */ public Contract createContract() { - boolean isBuyerMakerAndSellerTaker = getOffer().getDirection() == Direction.BUY; + boolean isBuyerMakerAndSellerTaker = getOffer().getDirection() == OfferDirection.BUY; Contract contract = new Contract( getOffer().getOfferPayload(), - checkNotNull(getTradeAmount()).value, - getTradePrice().getValue(), + checkNotNull(getAmount()).value, + getPrice().getValue(), isBuyerMakerAndSellerTaker ? getMakerNodeAddress() : getTakerNodeAddress(), // buyer node address // TODO (woodser): use maker and taker node address instead of buyer and seller node address for consistency isBuyerMakerAndSellerTaker ? getTakerNodeAddress() : getMakerNodeAddress(), // seller node address getArbitratorNodeAddress(), @@ -757,7 +757,7 @@ public abstract class Trade implements Tradable, Model { Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null"); BigInteger sellerDepositAmount = multisigWallet.getTx(this.getSeller().getDepositTxHash()).getIncomingAmount(); BigInteger buyerDepositAmount = multisigWallet.getTx(this.getBuyer().getDepositTxHash()).getIncomingAmount(); - BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(this.getTradeAmount()); + BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(this.getAmount()); BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount); BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount); @@ -807,7 +807,7 @@ public abstract class Trade implements Tradable, Model { Contract contract = getContract(); BigInteger sellerDepositAmount = multisigWallet.getTx(getSeller().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs this.getDepositTxId() necessary or avoidable? BigInteger buyerDepositAmount = multisigWallet.getTx(getBuyer().getDepositTxHash()).getIncomingAmount(); - BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(getTradeAmount()); + BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(getAmount()); // parse payout tx MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); @@ -990,6 +990,14 @@ public abstract class Trade implements Tradable, Model { } } + public boolean removeAllChatMessages() { + if (chatMessages.size() > 0) { + chatMessages.clear(); + return true; + } + return false; + } + public boolean mediationResultAppliedPenaltyToSeller() { // If mediated payout is same or more then normal payout we enable otherwise a penalty was applied // by mediators and we keep the confirm disabled to avoid that the seller can complete the trade @@ -999,6 +1007,15 @@ public abstract class Trade implements Tradable, Model { return payoutAmountFromMediation < normalPayoutAmount; } + public void maybeClearSensitiveData() { + String change = ""; + if (removeAllChatMessages()) { + change += "chat messages;"; + } + if (change.length() > 0) { + log.info("cleared sensitive data from {} of trade {}", change, getShortId()); + } + } /////////////////////////////////////////////////////////////////////////////////////////// // Model implementation @@ -1088,16 +1105,16 @@ public abstract class Trade implements Tradable, Model { refundResultStateProperty.set(refundResultState); } - public void setTradePeriodState(TradePeriodState tradePeriodState) { - this.tradePeriodState = tradePeriodState; + public void setPeriodState(TradePeriodState tradePeriodState) { + this.periodState = tradePeriodState; tradePeriodStateProperty.set(tradePeriodState); } - public void setTradeAmount(Coin tradeAmount) { + public void setAmount(Coin tradeAmount) { this.tradeAmount = tradeAmount; - tradeAmountAsLong = tradeAmount.value; - getTradeAmountProperty().set(tradeAmount); - getTradeVolumeProperty().set(getTradeVolume()); + amountAsLong = tradeAmount.value; + getAmountProperty().set(tradeAmount); + getVolumeProperty().set(getVolume()); } public void setPayoutTx(MoneroTxWallet payoutTx) { @@ -1121,11 +1138,11 @@ public abstract class Trade implements Tradable, Model { /////////////////////////////////////////////////////////////////////////////////////////// public boolean isBuyer() { - return offer.getDirection() == Direction.BUY; + return offer.getDirection() == OfferDirection.BUY; } public boolean isSeller() { - return offer.getDirection() == Direction.SELL; + return offer.getDirection() == OfferDirection.SELL; } public boolean isMaker() { @@ -1152,11 +1169,11 @@ public abstract class Trade implements Tradable, Model { } public TradingPeer getBuyer() { - return offer.getDirection() == Direction.BUY ? processModel.getMaker() : processModel.getTaker(); + return offer.getDirection() == OfferDirection.BUY ? processModel.getMaker() : processModel.getTaker(); } public TradingPeer getSeller() { - return offer.getDirection() == Direction.BUY ? processModel.getTaker() : processModel.getMaker(); + return offer.getDirection() == OfferDirection.BUY ? processModel.getTaker() : processModel.getMaker(); } /** @@ -1195,10 +1212,10 @@ public abstract class Trade implements Tradable, Model { } @Nullable - public Volume getTradeVolume() { + public Volume getVolume() { try { - if (getTradeAmount() != null && getTradePrice() != null) { - Volume volumeByAmount = getTradePrice().getVolumeByAmount(getTradeAmount()); + if (getAmount() != null && getPrice() != null) { + Volume volumeByAmount = getPrice().getVolumeByAmount(getAmount()); if (offer != null) { if (offer.getPaymentMethod().getId().equals(PaymentMethod.HAL_CASH_ID)) volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); @@ -1215,18 +1232,18 @@ public abstract class Trade implements Tradable, Model { } public Date getHalfTradePeriodDate() { - return new Date(getTradeStartTime() + getMaxTradePeriod() / 2); + return new Date(getStartTime() + getMaxTradePeriod() / 2); } public Date getMaxTradePeriodDate() { - return new Date(getTradeStartTime() + getMaxTradePeriod()); + return new Date(getStartTime() + getMaxTradePeriod()); } private long getMaxTradePeriod() { return getOffer().getPaymentMethod().getMaxTradePeriod(); } - private long getTradeStartTime() { + private long getStartTime() { long now = System.currentTimeMillis(); long startTime; final MoneroTx takerDepositTx = getTakerDepositTx(); @@ -1324,6 +1341,10 @@ public abstract class Trade implements Tradable, Model { return getState().getPhase().ordinal() >= Phase.PAYOUT_PUBLISHED.ordinal() || isWithdrawn(); } + public boolean isCompleted() { + return isPayoutPublished(); + } + public boolean isWithdrawn() { return getState().getPhase().ordinal() == Phase.WITHDRAWN.ordinal(); } @@ -1379,17 +1400,21 @@ public abstract class Trade implements Tradable, Model { return offer.getShortId(); } - public Price getTradePrice() { - return Price.valueOf(offer.getCurrencyCode(), tradePrice); + public Price getPrice() { + return Price.valueOf(offer.getCurrencyCode(), price); } @Nullable - public Coin getTradeAmount() { + public Coin getAmount() { if (tradeAmount == null) - tradeAmount = Coin.valueOf(tradeAmountAsLong); + tradeAmount = Coin.valueOf(amountAsLong); return tradeAmount; } + public Coin getMakerFee() { + return offer.getMakerFee(); + } + @Nullable public MoneroTxWallet getPayoutTx() { if (payoutTx == null) @@ -1433,17 +1458,17 @@ public abstract class Trade implements Tradable, Model { /////////////////////////////////////////////////////////////////////////////////////////// // lazy initialization - private ObjectProperty getTradeAmountProperty() { + private ObjectProperty getAmountProperty() { if (tradeAmountProperty == null) - tradeAmountProperty = getTradeAmount() != null ? new SimpleObjectProperty<>(getTradeAmount()) : new SimpleObjectProperty<>(); + tradeAmountProperty = getAmount() != null ? new SimpleObjectProperty<>(getAmount()) : new SimpleObjectProperty<>(); return tradeAmountProperty; } // lazy initialization - private ObjectProperty getTradeVolumeProperty() { + private ObjectProperty getVolumeProperty() { if (tradeVolumeProperty == null) - tradeVolumeProperty = getTradeVolume() != null ? new SimpleObjectProperty<>(getTradeVolume()) : new SimpleObjectProperty<>(); + tradeVolumeProperty = getVolume() != null ? new SimpleObjectProperty<>(getVolume()) : new SimpleObjectProperty<>(); return tradeVolumeProperty; } @@ -1493,11 +1518,11 @@ public abstract class Trade implements Tradable, Model { ",\n processModel=" + processModel + ",\n takerFeeTxId='" + takerFeeTxId + '\'' + ",\n payoutTxId='" + payoutTxId + '\'' + - ",\n tradeAmountAsLong=" + tradeAmountAsLong + - ",\n tradePrice=" + tradePrice + + ",\n tradeAmountAsLong=" + amountAsLong + + ",\n tradePrice=" + price + ",\n state=" + state + ",\n disputeState=" + disputeState + - ",\n tradePeriodState=" + tradePeriodState + + ",\n tradePeriodState=" + periodState + ",\n contract=" + contract + ",\n contractAsJson='" + contractAsJson + '\'' + ",\n contractHash=" + Utilities.bytesAsHexString(contractHash) + diff --git a/core/src/main/java/bisq/core/trade/TradeDataValidation.java b/core/src/main/java/bisq/core/trade/TradeDataValidation.java index bd277ebe64..54d2987771 100644 --- a/core/src/main/java/bisq/core/trade/TradeDataValidation.java +++ b/core/src/main/java/bisq/core/trade/TradeDataValidation.java @@ -292,7 +292,7 @@ public class TradeDataValidation { Offer offer = checkNotNull(trade.getOffer()); Coin msOutputAmount = offer.getBuyerSecurityDeposit() .add(offer.getSellerSecurityDeposit()) - .add(checkNotNull(trade.getTradeAmount())); + .add(checkNotNull(trade.getAmount())); if (!output.getValue().equals(msOutputAmount)) { errorMsg = "Output value of deposit tx and delayed payout tx is not matching. Output: " + output + " / msOutputAmount: " + msOutputAmount; diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index cb96a65fe2..d8a2ab07cf 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -23,7 +23,7 @@ import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; import bisq.core.offer.Offer; import bisq.core.offer.OfferBookService; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; @@ -35,7 +35,6 @@ import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.trade.Trade.Phase; -import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.failed.FailedTradesManager; import bisq.core.trade.handlers.TradeResultHandler; import bisq.core.trade.messages.DepositRequest; @@ -806,6 +805,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // If trade was completed (closed without fault but might be closed by a dispute) we move it to the closed trades public void onTradeCompleted(Trade trade) { closedTradableManager.add(trade); + trade.setState(Trade.State.WITHDRAW_COMPLETED); removeTrade(trade); // TODO The address entry should have been removed already. Check and if its the case remove that. @@ -855,10 +855,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (maxTradePeriodDate != null && halfTradePeriodDate != null) { Date now = new Date(); if (now.after(maxTradePeriodDate)) { - trade.setTradePeriodState(Trade.TradePeriodState.TRADE_PERIOD_OVER); + trade.setPeriodState(Trade.TradePeriodState.TRADE_PERIOD_OVER); requestPersistence(); } else if (now.after(halfTradePeriodDate)) { - trade.setTradePeriodState(Trade.TradePeriodState.SECOND_HALF); + trade.setPeriodState(Trade.TradePeriodState.SECOND_HALF); requestPersistence(); } } @@ -998,11 +998,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } public boolean isBuyer(Offer offer) { - // If I am the maker, we use the OfferPayload.Direction, otherwise the mirrored direction + // If I am the maker, we use the OfferDirection, otherwise the mirrored direction if (isMyOffer(offer)) return offer.isBuyOffer(); else - return offer.getDirection() == OfferPayload.Direction.SELL; + return offer.getDirection() == OfferDirection.SELL; } // TODO (woodser): make Optional versus Trade return types consistent diff --git a/core/src/main/java/bisq/core/trade/TradeModule.java b/core/src/main/java/bisq/core/trade/TradeModule.java index 5d1590383b..10fde10ecd 100644 --- a/core/src/main/java/bisq/core/trade/TradeModule.java +++ b/core/src/main/java/bisq/core/trade/TradeModule.java @@ -21,7 +21,6 @@ import bisq.core.account.sign.SignedWitnessService; import bisq.core.account.sign.SignedWitnessStorageService; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.account.witness.AccountAgeWitnessStorageService; -import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.failed.FailedTradesManager; import bisq.core.trade.statistics.ReferralIdService; diff --git a/core/src/main/java/bisq/core/trade/TradeUtils.java b/core/src/main/java/bisq/core/trade/TradeUtils.java index d52f34ec20..14773562e4 100644 --- a/core/src/main/java/bisq/core/trade/TradeUtils.java +++ b/core/src/main/java/bisq/core/trade/TradeUtils.java @@ -26,6 +26,7 @@ import bisq.core.btc.wallet.XmrWalletService; import bisq.core.offer.OfferPayload; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.trade.messages.InitTradeRequest; +import bisq.core.util.JsonUtil; import java.util.Objects; import java.util.concurrent.CountDownLatch; @@ -55,7 +56,7 @@ public class TradeUtils { signedOfferPayload.setArbitratorSignature(null); // get unsigned offer payload as json string - String unsignedOfferAsJson = Utilities.objectToJson(signedOfferPayload); + String unsignedOfferAsJson = JsonUtil.objectToJson(signedOfferPayload); // verify arbitrator signature boolean isValid = true; @@ -108,7 +109,7 @@ public class TradeUtils { ); // get trade request as string - String tradeRequestAsJson = Utilities.objectToJson(signedRequest); + String tradeRequestAsJson = JsonUtil.objectToJson(signedRequest); // verify maker signature try { diff --git a/core/src/main/java/bisq/core/trade/closed/ClosedTradableManager.java b/core/src/main/java/bisq/core/trade/closed/ClosedTradableManager.java deleted file mode 100644 index 270df82d82..0000000000 --- a/core/src/main/java/bisq/core/trade/closed/ClosedTradableManager.java +++ /dev/null @@ -1,124 +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 . - */ - -package bisq.core.trade.closed; - -import bisq.core.offer.Offer; -import bisq.core.provider.price.PriceFeedService; -import bisq.core.trade.DumpDelayedPayoutTx; -import bisq.core.trade.Tradable; -import bisq.core.trade.TradableList; -import bisq.core.trade.Trade; - -import bisq.common.crypto.KeyRing; -import bisq.common.persistence.PersistenceManager; -import bisq.common.proto.persistable.PersistedDataHost; - -import com.google.inject.Inject; - -import com.google.common.collect.ImmutableList; - -import javafx.collections.ObservableList; - -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class ClosedTradableManager implements PersistedDataHost { - private final PersistenceManager> persistenceManager; - private final TradableList closedTradables = new TradableList<>(); - private final KeyRing keyRing; - private final PriceFeedService priceFeedService; - private final CleanupMailboxMessages cleanupMailboxMessages; - private final DumpDelayedPayoutTx dumpDelayedPayoutTx; - - @Inject - public ClosedTradableManager(KeyRing keyRing, - PriceFeedService priceFeedService, - PersistenceManager> persistenceManager, - CleanupMailboxMessages cleanupMailboxMessages, - DumpDelayedPayoutTx dumpDelayedPayoutTx) { - this.keyRing = keyRing; - this.priceFeedService = priceFeedService; - this.cleanupMailboxMessages = cleanupMailboxMessages; - this.dumpDelayedPayoutTx = dumpDelayedPayoutTx; - this.persistenceManager = persistenceManager; - - this.persistenceManager.initialize(closedTradables, "ClosedTrades", PersistenceManager.Source.PRIVATE); - } - - @Override - public void readPersisted(Runnable completeHandler) { - persistenceManager.readPersisted(persisted -> { - closedTradables.setAll(persisted.getList()); - closedTradables.stream() - .filter(tradable -> tradable.getOffer() != null) - .forEach(tradable -> tradable.getOffer().setPriceFeedService(priceFeedService)); - dumpDelayedPayoutTx.maybeDumpDelayedPayoutTxs(closedTradables, "delayed_payout_txs_closed"); - completeHandler.run(); - }, - completeHandler); - } - - public void onAllServicesInitialized() { - cleanupMailboxMessages.handleTrades(getClosedTrades()); - } - - public void add(Tradable tradable) { - if (closedTradables.add(tradable)) { - requestPersistence(); - } - } - - public void remove(Tradable tradable) { - if (closedTradables.remove(tradable)) { - requestPersistence(); - } - } - - public boolean wasMyOffer(Offer offer) { - return offer.isMyOffer(keyRing); - } - - public ObservableList getObservableList() { - return closedTradables.getObservableList(); - } - - public List getClosedTrades() { - return ImmutableList.copyOf(getObservableList().stream() - .filter(e -> e instanceof Trade) - .map(e -> (Trade) e) - .collect(Collectors.toList())); - } - - public Optional getTradableById(String id) { - return closedTradables.stream().filter(e -> e.getId().equals(id)).findFirst(); - } - - public Stream getTradesStreamWithFundsLockedIn() { - return getClosedTrades().stream() - .filter(Trade::isFundsLockedIn); - } - - private void requestPersistence() { - persistenceManager.requestPersistence(); - } -} diff --git a/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java b/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java index 57914cf20f..655ea8f29a 100644 --- a/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java +++ b/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java @@ -21,12 +21,11 @@ import bisq.core.btc.model.XmrAddressEntry; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.offer.Offer; import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.CleanupMailboxMessages; import bisq.core.trade.DumpDelayedPayoutTx; import bisq.core.trade.TradableList; import bisq.core.trade.Trade; import bisq.core.trade.TradeUtil; -import bisq.core.trade.closed.CleanupMailboxMessages; - import bisq.common.crypto.KeyRing; import bisq.common.persistence.PersistenceManager; import bisq.common.proto.persistable.PersistedDataHost; diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesDepositRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesDepositRequest.java index c792a9c826..1ae5380719 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesDepositRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesDepositRequest.java @@ -24,6 +24,7 @@ import bisq.common.crypto.Sig; import bisq.common.taskrunner.TaskRunner; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferPayload; import bisq.core.trade.Trade; import bisq.core.trade.messages.DepositRequest; @@ -75,7 +76,7 @@ public class ArbitratorProcessesDepositRequest extends TradeTask { // collect expected values of deposit tx Offer offer = trade.getOffer(); boolean isFromTaker = request.getSenderNodeAddress().equals(trade.getTakerNodeAddress()); - boolean isFromBuyer = isFromTaker ? offer.getDirection() == OfferPayload.Direction.SELL : offer.getDirection() == OfferPayload.Direction.BUY; + boolean isFromBuyer = isFromTaker ? offer.getDirection() == OfferDirection.SELL : offer.getDirection() == OfferDirection.BUY; BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit())); String depositAddress = processModel.getMultisigAddress(); BigInteger tradeFee; diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesReserveTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesReserveTx.java index 7cc579e227..47bb93d611 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesReserveTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesReserveTx.java @@ -19,6 +19,7 @@ package bisq.core.trade.protocol.tasks; import bisq.common.taskrunner.TaskRunner; import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferPayload; import bisq.core.trade.Trade; import bisq.core.trade.TradeUtils; @@ -48,7 +49,7 @@ public class ArbitratorProcessesReserveTx extends TradeTask { Offer offer = trade.getOffer(); InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage(); boolean isFromTaker = request.getSenderNodeAddress().equals(trade.getTakerNodeAddress()); - boolean isFromBuyer = isFromTaker ? offer.getDirection() == OfferPayload.Direction.SELL : offer.getDirection() == OfferPayload.Direction.BUY; + boolean isFromBuyer = isFromTaker ? offer.getDirection() == OfferDirection.SELL : offer.getDirection() == OfferDirection.BUY; // TODO (woodser): if signer online, should never be called by maker diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorSendsInitTradeAndMultisigRequests.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorSendsInitTradeAndMultisigRequests.java index 1884dbfba1..ac7a660a37 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorSendsInitTradeAndMultisigRequests.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorSendsInitTradeAndMultisigRequests.java @@ -75,8 +75,8 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask { processModel.getOfferId(), request.getSenderNodeAddress(), request.getPubKeyRing(), - trade.getTradeAmount().value, - trade.getTradePrice().getValue(), + trade.getAmount().value, + trade.getPrice().getValue(), trade.getTakerFee().getValue(), request.getAccountId(), request.getPaymentAccountId(), diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitTradeRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitTradeRequest.java index 0514ed98fa..eea02e38ec 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitTradeRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitTradeRequest.java @@ -109,8 +109,8 @@ public class ProcessInitTradeRequest extends TradeTask { // check trade price try { long tradePrice = request.getTradePrice(); - offer.checkTradePriceTolerance(tradePrice); - trade.setTradePrice(tradePrice); + offer.verifyTakersTradePrice(tradePrice); + trade.setPrice(tradePrice); } catch (TradePriceOutOfToleranceException e) { failed(e.getMessage()); } catch (Throwable e2) { @@ -119,7 +119,7 @@ public class ProcessInitTradeRequest extends TradeTask { // check trade amount checkArgument(request.getTradeAmount() > 0); - trade.setTradeAmount(Coin.valueOf(request.getTradeAmount())); + trade.setAmount(Coin.valueOf(request.getTradeAmount())); // persist trade processModel.getTradeManager().requestPersistence(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractRequest.java index 66de7983a5..8263c64bef 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractRequest.java @@ -30,6 +30,7 @@ import bisq.core.trade.Trade.State; import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.protocol.TradingPeer; +import bisq.core.util.JsonUtil; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.SendDirectMessageListener; import java.util.Date; @@ -71,7 +72,7 @@ public class ProcessSignContractRequest extends TradeTask { // create and sign contract Contract contract = trade.createContract(); - String contractAsJson = Utilities.objectToJson(contract); + String contractAsJson = JsonUtil.objectToJson(contract); String signature = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), contractAsJson); // save contract and signature diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/PublishTradeStatistics.java b/core/src/main/java/bisq/core/trade/protocol/tasks/PublishTradeStatistics.java index 2f83b282ac..78e4615ba8 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/PublishTradeStatistics.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/PublishTradeStatistics.java @@ -66,8 +66,8 @@ public class PublishTradeStatistics extends TradeTask { Offer offer = checkNotNull(trade.getOffer()); TradeStatistics2 tradeStatistics = new TradeStatistics2(offer.getOfferPayload(), - trade.getTradePrice(), - trade.getTradeAmount(), + trade.getPrice(), + trade.getAmount(), trade.getDate(), trade.getMaker().getDepositTxHash(), trade.getTaker().getDepositTxHash(), diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerPreparesPaymentSentMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerPreparesPaymentSentMessage.java index f97e1e2724..7cbb4db42f 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerPreparesPaymentSentMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerPreparesPaymentSentMessage.java @@ -55,7 +55,7 @@ public class BuyerPreparesPaymentSentMessage extends TradeTask { runInterceptHook(); // validate state - Preconditions.checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); + Preconditions.checkNotNull(trade.getAmount(), "trade.getTradeAmount() must not be null"); Preconditions.checkNotNull(trade.getMakerDepositTx(), "trade.getMakerDepositTx() must not be null"); Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null"); checkNotNull(trade.getOffer(), "offer must not be null"); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java index 389583149b..c3b562bd31 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java @@ -52,7 +52,7 @@ public class BuyerAsMakerCreatesAndSignsDepositTx extends TradeTask { protected void run() { try { runInterceptHook(); - Coin tradeAmount = checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); + Coin tradeAmount = checkNotNull(trade.getAmount(), "trade.getTradeAmount() must not be null"); BtcWalletService walletService = processModel.getBtcWalletService(); String id = processModel.getOffer().getId(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetsLockTime.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetsLockTime.java index a08a8d988c..973e9c2a24 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetsLockTime.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetsLockTime.java @@ -41,7 +41,7 @@ public class MakerSetsLockTime extends TradeTask { // For regtest dev environment we use 5 blocks int delay = Config.baseCurrencyNetwork().isStagenet() ? 5 : - Restrictions.getLockTime(processModel.getOffer().getPaymentMethod().isAsset()); + Restrictions.getLockTime(processModel.getOffer().getPaymentMethod().isBlockchain()); long lockTime = processModel.getBtcWalletService().getBestChainHeight() + delay; log.info("lockTime={}, delay={}", lockTime, delay); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesUnsignedDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesUnsignedDepositTx.java index 2048563453..737521a6a1 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesUnsignedDepositTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesUnsignedDepositTx.java @@ -51,7 +51,7 @@ public class SellerAsMakerCreatesUnsignedDepositTx extends TradeTask { protected void run() { try { runInterceptHook(); - checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); + checkNotNull(trade.getAmount(), "trade.getTradeAmount() must not be null"); BtcWalletService walletService = processModel.getBtcWalletService(); String id = processModel.getOffer().getId(); @@ -66,7 +66,7 @@ public class SellerAsMakerCreatesUnsignedDepositTx extends TradeTask { + trade.getContractAsJson() + "\n------------------------------------------------------------\n"); - Coin makerInputAmount = offer.getSellerSecurityDeposit().add(trade.getTradeAmount()); + Coin makerInputAmount = offer.getSellerSecurityDeposit().add(trade.getAmount()); Optional addressEntryOptional = walletService.getAddressEntry(id, AddressEntry.Context.MULTI_SIG); checkArgument(addressEntryOptional.isPresent(), "addressEntryOptional must be present"); AddressEntry makerMultiSigAddressEntry = addressEntryOptional.get(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignsDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignsDepositTx.java index 8deec1b36b..c29542b468 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignsDepositTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignsDepositTx.java @@ -74,7 +74,7 @@ public class SellerAsTakerSignsDepositTx extends TradeTask { Offer offer = trade.getOffer(); Coin msOutputAmount = offer.getBuyerSecurityDeposit().add(offer.getSellerSecurityDeposit()).add(trade.getTxFee()) - .add(checkNotNull(trade.getTradeAmount())); + .add(checkNotNull(trade.getAmount())); TradingPeer tradingPeer = trade.getTradingPeer(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessesInputsForDepositTxResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessesInputsForDepositTxResponse.java index 91b3cddd8a..ba47dbb6da 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessesInputsForDepositTxResponse.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessesInputsForDepositTxResponse.java @@ -60,7 +60,7 @@ public class TakerProcessesInputsForDepositTxResponse extends TradeTask { long lockTime = response.getLockTime(); if (Config.baseCurrencyNetwork().isMainnet()) { int myLockTime = processModel.getBtcWalletService().getBestChainHeight() + - Restrictions.getLockTime(processModel.getOffer().getPaymentMethod().isAsset()); + Restrictions.getLockTime(processModel.getOffer().getPaymentMethod().isBlockchain()); // We allow a tolerance of 3 blocks as BestChainHeight might be a bit different on maker and taker in case a new // block was just found checkArgument(Math.abs(lockTime - myLockTime) <= 3, diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java index d12f3078b0..8ab7915c44 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java @@ -22,8 +22,10 @@ import bisq.core.monetary.AltcoinExchangeRate; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferPayload; import bisq.core.trade.Trade; +import bisq.core.util.JsonUtil; import bisq.core.util.VolumeUtil; import bisq.network.p2p.NodeAddress; @@ -92,10 +94,10 @@ public final class TradeStatistics2 implements ProcessOncePersistableNetworkPayl Offer offer = trade.getOffer(); checkNotNull(offer, "offer must not ne null"); - checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not ne null"); + checkNotNull(trade.getAmount(), "trade.getTradeAmount() must not ne null"); return new TradeStatistics2(offer.getOfferPayload(), - trade.getTradePrice(), - trade.getTradeAmount(), + trade.getPrice(), + trade.getAmount(), trade.getDate(), trade.getMaker().getDepositTxHash(), trade.getTaker().getDepositTxHash(), @@ -107,7 +109,7 @@ public final class TradeStatistics2 implements ProcessOncePersistableNetworkPayl @SuppressWarnings("SpellCheckingInspection") public static final String REFUND_AGENT_ADDRESS = "refAddr"; - private final OfferPayload.Direction direction; + private final OfferDirection direction; private final String baseCurrency; private final String counterCurrency; private final String offerPaymentMethod; @@ -153,7 +155,7 @@ public final class TradeStatistics2 implements ProcessOncePersistableNetworkPayl offerPayload.getPaymentMethodId(), offerPayload.getDate(), offerPayload.isUseMarketBasedPrice(), - offerPayload.getMarketPriceMargin(), + offerPayload.getMarketPriceMarginPct(), offerPayload.getAmount(), offerPayload.getMinAmount(), offerPayload.getId(), @@ -170,7 +172,7 @@ public final class TradeStatistics2 implements ProcessOncePersistableNetworkPayl // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - public TradeStatistics2(OfferPayload.Direction direction, + public TradeStatistics2(OfferDirection direction, String baseCurrency, String counterCurrency, String offerPaymentMethod, @@ -211,12 +213,12 @@ public final class TradeStatistics2 implements ProcessOncePersistableNetworkPayl // We create hash from all fields excluding hash itself. We use json as simple data serialisation. // TradeDate is different for both peers so we ignore it for hash. ExtraDataMap is ignored as well as at // software updates we might have different entries which would cause a different hash. - return Hash.getSha256Ripemd160hash(Utilities.objectToJson(this).getBytes(Charsets.UTF_8)); + return Hash.getSha256Ripemd160hash(JsonUtil.objectToJson(this).getBytes(Charsets.UTF_8)); } private protobuf.TradeStatistics2.Builder getBuilder() { final protobuf.TradeStatistics2.Builder builder = protobuf.TradeStatistics2.newBuilder() - .setDirection(OfferPayload.Direction.toProtoMessage(direction)) + .setDirection(OfferDirection.toProtoMessage(direction)) .setBaseCurrency(baseCurrency) .setCounterCurrency(counterCurrency) .setPaymentMethodId(offerPaymentMethod) @@ -247,7 +249,7 @@ public final class TradeStatistics2 implements ProcessOncePersistableNetworkPayl public static TradeStatistics2 fromProto(protobuf.TradeStatistics2 proto) { return new TradeStatistics2( - OfferPayload.Direction.fromProto(proto.getDirection()), + OfferDirection.fromProto(proto.getDirection()), proto.getBaseCurrency(), proto.getCounterCurrency(), proto.getPaymentMethodId(), @@ -302,7 +304,7 @@ public final class TradeStatistics2 implements ProcessOncePersistableNetworkPayl return new Date(tradeDate); } - public Price getTradePrice() { + public Price getPrice() { return Price.valueOf(getCurrencyCode(), tradePrice); } @@ -315,10 +317,10 @@ public final class TradeStatistics2 implements ProcessOncePersistableNetworkPayl } public Volume getTradeVolume() { - if (getTradePrice().getMonetary() instanceof Altcoin) { - return new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount())); + if (getPrice().getMonetary() instanceof Altcoin) { + return new Volume(new AltcoinExchangeRate((Altcoin) getPrice().getMonetary()).coinToAltcoin(getTradeAmount())); } else { - Volume volume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount())); + Volume volume = new Volume(new ExchangeRate((Fiat) getPrice().getMonetary()).coinToFiat(getTradeAmount())); return VolumeUtil.getRoundedFiatVolume(volume); } } diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java index c0ec41ec1f..4aca2d6f61 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java @@ -24,6 +24,7 @@ import bisq.core.monetary.Volume; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.trade.Trade; +import bisq.core.util.JsonUtil; import bisq.core.util.VolumeUtil; import bisq.network.p2p.NodeAddress; @@ -96,8 +97,8 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl Offer offer = checkNotNull(trade.getOffer()); return new TradeStatistics3(offer.getCurrencyCode(), - trade.getTradePrice().getValue(), - trade.getTradeAmountAsLong(), + trade.getPrice().getValue(), + trade.getAmountAsLong(), offer.getPaymentMethod().getId(), trade.getTakeOfferDate().getTime(), truncatedArbitratorNodeAddress, @@ -143,7 +144,28 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl BLOCK_CHAINS_INSTANT, TRANSFERWISE, AMAZON_GIFT_CARD, - CASH_BY_MAIL + CASH_BY_MAIL, + CAPITUAL, + PAYSERA, + PAXUM, + SWIFT, + NEFT, + RTGS, + IMPS, + UPI, + PAYTM, + CELPAY, + NEQUI, + BIZUM, + PIX, + MONESE, + SATISPAY, + VERSE, + STRIKE, + TIKKIE, + TRANSFERWISE_USD, + ACH_TRANSFER, + DOMESTIC_WIRE_TRANSFER } @Getter @@ -255,7 +277,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl // We create hash from all fields excluding hash itself. We use json as simple data serialisation. // TradeDate is different for both peers so we ignore it for hash. ExtraDataMap is ignored as well as at // software updates we might have different entries which would cause a different hash. - return Hash.getSha256Ripemd160hash(Utilities.objectToJson(this).getBytes(Charsets.UTF_8)); + return Hash.getSha256Ripemd160hash(JsonUtil.objectToJson(this).getBytes(Charsets.UTF_8)); } private protobuf.TradeStatistics3.Builder getBuilder() { @@ -338,7 +360,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl arbitrator = null; } - public String getPaymentMethod() { + public String getPaymentMethodId() { try { return PaymentMethodMapper.values()[Integer.parseInt(paymentMethod)].name(); } catch (Throwable ignore) { diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsConverter.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsConverter.java index 2b44439d32..c8a3c8954e 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsConverter.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsConverter.java @@ -167,7 +167,7 @@ public class TradeStatisticsConverter { // for both traders as it excluded the trade date which is different for both. byte[] hash = tradeStatistics2.getHash(); return new TradeStatistics3(tradeStatistics2.getCurrencyCode(), - tradeStatistics2.getTradePrice().getValue(), + tradeStatistics2.getPrice().getValue(), tradeStatistics2.getTradeAmount().getValue(), tradeStatistics2.getOfferPaymentMethod(), time, diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsForJson.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsForJson.java index 8b311a5325..5833fbc093 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsForJson.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsForJson.java @@ -52,13 +52,13 @@ public final class TradeStatisticsForJson { public TradeStatisticsForJson(TradeStatistics3 tradeStatistics) { this.currency = tradeStatistics.getCurrency(); - this.paymentMethod = tradeStatistics.getPaymentMethod(); + this.paymentMethod = tradeStatistics.getPaymentMethodId(); this.tradePrice = tradeStatistics.getPrice(); this.tradeAmount = tradeStatistics.getAmount(); this.tradeDate = tradeStatistics.getDateAsLong(); try { - Price tradePrice = getTradePrice(); + Price tradePrice = getPrice(); if (CurrencyUtil.isCryptoCurrency(currency)) { currencyPair = currency + "/" + Res.getBaseCurrencyCode(); primaryMarketTradePrice = tradePrice.getValue(); @@ -82,7 +82,7 @@ public final class TradeStatisticsForJson { } } - public Price getTradePrice() { + public Price getPrice() { return Price.valueOf(currency, tradePrice); } @@ -92,7 +92,7 @@ public final class TradeStatisticsForJson { public Volume getTradeVolume() { try { - return getTradePrice().getVolumeByAmount(getTradeAmount()); + return getPrice().getVolumeByAmount(getTradeAmount()); } catch (Throwable t) { return Volume.parse("0", currency); } diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java index 8c785ab811..1a34a0f240 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java @@ -23,7 +23,7 @@ import bisq.core.locale.Res; import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.BuyerTrade; import bisq.core.trade.Trade; - +import bisq.core.util.JsonUtil; import bisq.network.p2p.P2PService; import bisq.network.p2p.storage.P2PDataStorage; import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; @@ -131,13 +131,13 @@ public class TradeStatisticsManager { ArrayList fiatCurrencyList = CurrencyUtil.getAllSortedFiatCurrencies().stream() .map(e -> new CurrencyTuple(e.getCode(), e.getName(), 8)) .collect(Collectors.toCollection(ArrayList::new)); - jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(fiatCurrencyList), "fiat_currency_list"); + jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(fiatCurrencyList), "fiat_currency_list"); ArrayList cryptoCurrencyList = CurrencyUtil.getAllSortedCryptoCurrencies().stream() .map(e -> new CurrencyTuple(e.getCode(), e.getName(), 8)) .collect(Collectors.toCollection(ArrayList::new)); cryptoCurrencyList.add(0, new CurrencyTuple(Res.getBaseCurrencyCode(), Res.getBaseCurrencyName(), 8)); - jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(cryptoCurrencyList), "crypto_currency_list"); + jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(cryptoCurrencyList), "crypto_currency_list"); Instant yearAgo = Instant.ofEpochSecond(Instant.now().getEpochSecond() - TimeUnit.DAYS.toSeconds(365)); Set activeCurrencies = observableTradeStatisticsSet.stream() @@ -149,13 +149,13 @@ public class TradeStatisticsManager { .filter(e -> activeCurrencies.contains(e.code)) .map(e -> new CurrencyTuple(e.code, e.name, 8)) .collect(Collectors.toCollection(ArrayList::new)); - jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(activeFiatCurrencyList), "active_fiat_currency_list"); + jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(activeFiatCurrencyList), "active_fiat_currency_list"); ArrayList activeCryptoCurrencyList = cryptoCurrencyList.stream() .filter(e -> activeCurrencies.contains(e.code)) .map(e -> new CurrencyTuple(e.code, e.name, 8)) .collect(Collectors.toCollection(ArrayList::new)); - jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(activeCryptoCurrencyList), "active_crypto_currency_list"); + jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(activeCryptoCurrencyList), "active_crypto_currency_list"); } List list = observableTradeStatisticsSet.stream() @@ -164,7 +164,7 @@ public class TradeStatisticsManager { .collect(Collectors.toList()); TradeStatisticsForJson[] array = new TradeStatisticsForJson[list.size()]; list.toArray(array); - jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(array), "trade_statistics"); + jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(array), "trade_statistics"); } public void maybeRepublishTradeStatistics(Set trades, diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofModel.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofModel.java index 49770ec763..002d4d2481 100644 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofModel.java +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofModel.java @@ -60,7 +60,7 @@ public class XmrTxProofModel implements AssetTxProofModel { this.serviceAddress = serviceAddress; this.autoConfirmSettings = autoConfirmSettings; - Volume volume = trade.getTradeVolume(); + Volume volume = trade.getVolume(); amount = DevEnv.isDevMode() ? XmrTxProofModel.DEV_AMOUNT : // For dev testing we need to add the matching address to the dev tx key and dev view key volume != null ? volume.getValue() * 10000L : 0L; // XMR satoshis have 12 decimal places vs. bitcoin's 8 diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java index 7d0491655c..766c8d5646 100644 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java @@ -327,7 +327,7 @@ class XmrTxProofRequestsPerTrade implements AssetTxProofRequestsPerTrade { /////////////////////////////////////////////////////////////////////////////////////////// private boolean isTradeAmountAboveLimit(Trade trade) { - Coin tradeAmount = trade.getTradeAmount(); + Coin tradeAmount = trade.getAmount(); Coin tradeLimit = Coin.valueOf(autoConfirmSettings.getTradeLimit()); if (tradeAmount != null && tradeAmount.isGreaterThan(tradeLimit)) { log.warn("Trade amount {} is higher than limit from auto-conf setting {}.", diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java index 671c17bdf1..22604c481c 100644 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java @@ -22,10 +22,10 @@ import bisq.core.filter.FilterManager; import bisq.core.locale.Res; import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.support.dispute.refund.RefundManager; +import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.SellerTrade; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; -import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.failed.FailedTradesManager; import bisq.core.trade.protocol.SellerProtocol; import bisq.core.trade.txproof.AssetTxProofResult; diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index c54141a80f..a94b3243fa 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -123,7 +123,6 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid )); private static final ArrayList XMR_TX_PROOF_SERVICES = new ArrayList<>(Arrays.asList( "monero3bec7m26vx6si6qo7q7imlaoz45ot5m2b5z2ppgoooo6jx2rqd.onion", // @emzy - //"wizxmr4hbdxdszqm5rfyqvceyca5jq62ppvtuznasnk66wvhhvgm3uyd.onion", // @wiz "devinxmrwu4jrfq2zmq5kqjpxb44hx7i7didebkwrtvmvygj4uuop2ad.onion" // @devinbileck )); @@ -141,6 +140,8 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid )); public static final boolean USE_SYMMETRIC_SECURITY_DEPOSIT = true; + public static final int CLEAR_DATA_AFTER_DAYS_INITIAL = 99999; // feature effectively disabled until user agrees to settings notification + public static final int CLEAR_DATA_AFTER_DAYS_DEFAULT = 60; // used when user has agreed to settings notification // payload is initialized so the default values are available for Property initialization. @@ -239,6 +240,15 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid setFiatCurrencies(prefPayload.getFiatCurrencies()); setCryptoCurrencies(prefPayload.getCryptoCurrencies()); GlobalSettings.setDefaultTradeCurrency(preferredTradeCurrency); + + // If a user has updated and the field was not set and get set to 0 by protobuf + // As there is no way to detect that a primitive value field was set we cannot apply + // a "marker" value like -1 to it. We also do not want to wrap the value in a new + // proto message as thats too much for that feature... So we accept that if the user + // sets the value to 0 it will be overwritten by the default at next startup. + if (prefPayload.getBsqAverageTrimThreshold() == 0) { + prefPayload.setBsqAverageTrimThreshold(0.05); + } setupPreferences(); } @@ -248,8 +258,14 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid prefPayload.setUserLanguage(GlobalSettings.getLocale().getLanguage()); prefPayload.setUserCountry(CountryUtil.getDefaultCountry()); GlobalSettings.setLocale(new Locale(prefPayload.getUserLanguage(), prefPayload.getUserCountry().code)); - TradeCurrency preferredTradeCurrency = checkNotNull(CurrencyUtil.getCurrencyByCountryCode(prefPayload.getUserCountry().code), - "preferredTradeCurrency must not be null"); + + TradeCurrency preferredTradeCurrency = CurrencyUtil.getCurrencyByCountryCode("US"); // default fallback option + try { + preferredTradeCurrency = CurrencyUtil.getCurrencyByCountryCode(prefPayload.getUserCountry().code); + } catch (IllegalArgumentException ia) { + log.warn("Could not determine currency for country {} [{}]", prefPayload.getUserCountry().code, ia.toString()); + } + prefPayload.setPreferredTradeCurrency(preferredTradeCurrency); setFiatCurrencies(CurrencyUtil.getMainFiatCurrencies()); setCryptoCurrencies(CurrencyUtil.getMainCryptoCurrencies()); @@ -417,6 +433,11 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid requestPersistence(); } + public void setBsqAverageTrimThreshold(double bsqAverageTrimThreshold) { + prefPayload.setBsqAverageTrimThreshold(bsqAverageTrimThreshold); + requestPersistence(); + } + public Optional findAutoConfirmSettings(String currencyCode) { return prefPayload.getAutoConfirmSettingsList().stream() .filter(e -> e.getCurrencyCode().equals(currencyCode)) @@ -533,6 +554,16 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid requestPersistence(); } + public void setBuyScreenCryptoCurrencyCode(String buyScreenCurrencyCode) { + prefPayload.setBuyScreenCryptoCurrencyCode(buyScreenCurrencyCode); + requestPersistence(); + } + + public void setSellScreenCryptoCurrencyCode(String sellScreenCurrencyCode) { + prefPayload.setSellScreenCryptoCurrencyCode(sellScreenCurrencyCode); + requestPersistence(); + } + public void setIgnoreTradersList(List ignoreTradersList) { prefPayload.setIgnoreTradersList(ignoreTradersList); requestPersistence(); @@ -620,7 +651,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid public void setBridgeAddresses(List bridgeAddresses) { prefPayload.setBridgeAddresses(bridgeAddresses); // We call that before shutdown so we dont want a delay here - requestPersistence(); + persistenceManager.forcePersistNow(); } // Only used from PB but keep it explicit as it may be used from the client and then we want to persist @@ -631,17 +662,17 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid public void setBridgeOptionOrdinal(int bridgeOptionOrdinal) { prefPayload.setBridgeOptionOrdinal(bridgeOptionOrdinal); - requestPersistence(); + persistenceManager.forcePersistNow(); } public void setTorTransportOrdinal(int torTransportOrdinal) { prefPayload.setTorTransportOrdinal(torTransportOrdinal); - requestPersistence(); + persistenceManager.forcePersistNow(); } public void setCustomBridges(String customBridges) { prefPayload.setCustomBridges(customBridges); - requestPersistence(); + persistenceManager.forcePersistNow(); } public void setBitcoinNodesOptionOrdinal(int bitcoinNodesOptionOrdinal) { @@ -693,6 +724,11 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid requestPersistence(); } + public void setClearDataAfterDays(int value) { + prefPayload.setClearDataAfterDays(value); + requestPersistence(); + } + public void setShowOffersMatchingMyAccounts(boolean value) { prefPayload.setShowOffersMatchingMyAccounts(value); requestPersistence(); @@ -952,6 +988,8 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid void setTacAcceptedV120(boolean tacAccepted); + void setBsqAverageTrimThreshold(double bsqAverageTrimThreshold); + void setAutoConfirmSettings(AutoConfirmSettings autoConfirmSettings); void setHideNonAccountPaymentMethods(boolean hideNonAccountPaymentMethods); diff --git a/core/src/main/java/bisq/core/user/PreferencesPayload.java b/core/src/main/java/bisq/core/user/PreferencesPayload.java index c5b1922d2e..6824dafc5d 100644 --- a/core/src/main/java/bisq/core/user/PreferencesPayload.java +++ b/core/src/main/java/bisq/core/user/PreferencesPayload.java @@ -77,6 +77,10 @@ public final class PreferencesPayload implements PersistableEnvelope { private String buyScreenCurrencyCode; @Nullable private String sellScreenCurrencyCode; + @Nullable + private String buyScreenCryptoCurrencyCode; + @Nullable + private String sellScreenCryptoCurrencyCode; private int tradeStatisticsTickUnitIndex = 3; private boolean resyncSpvRequested; private boolean sortMarketCurrenciesNumerically = true; @@ -118,9 +122,11 @@ public final class PreferencesPayload implements PersistableEnvelope { private String takeOfferSelectedPaymentAccountId; private double buyerSecurityDepositAsPercent = getDefaultBuyerSecurityDepositAsPercent(); private int ignoreDustThreshold = 600; + private int clearDataAfterDays = Preferences.CLEAR_DATA_AFTER_DAYS_INITIAL; private double buyerSecurityDepositAsPercentForCrypto = getDefaultBuyerSecurityDepositAsPercent(); private int blockNotifyPort; private boolean tacAcceptedV120; + private double bsqAverageTrimThreshold = 0.05; // Added at 1.3.8 private List autoConfirmSettingsList = new ArrayList<>(); @@ -187,9 +193,11 @@ public final class PreferencesPayload implements PersistableEnvelope { .setUseStandbyMode(useStandbyMode) .setBuyerSecurityDepositAsPercent(buyerSecurityDepositAsPercent) .setIgnoreDustThreshold(ignoreDustThreshold) + .setClearDataAfterDays(clearDataAfterDays) .setBuyerSecurityDepositAsPercentForCrypto(buyerSecurityDepositAsPercentForCrypto) .setBlockNotifyPort(blockNotifyPort) .setTacAcceptedV120(tacAcceptedV120) + .setBsqAverageTrimThreshold(bsqAverageTrimThreshold) .addAllAutoConfirmSettings(autoConfirmSettingsList.stream() .map(autoConfirmSettings -> ((protobuf.AutoConfirmSettings) autoConfirmSettings.toProtoMessage())) .collect(Collectors.toList())) @@ -204,7 +212,10 @@ public final class PreferencesPayload implements PersistableEnvelope { Optional.ofNullable(tradeChartsScreenCurrencyCode).ifPresent(builder::setTradeChartsScreenCurrencyCode); Optional.ofNullable(buyScreenCurrencyCode).ifPresent(builder::setBuyScreenCurrencyCode); Optional.ofNullable(sellScreenCurrencyCode).ifPresent(builder::setSellScreenCurrencyCode); - Optional.ofNullable(selectedPaymentAccountForCreateOffer).ifPresent(account -> builder.setSelectedPaymentAccountForCreateOffer(account.toProtoMessage())); + Optional.ofNullable(buyScreenCryptoCurrencyCode).ifPresent(builder::setBuyScreenCryptoCurrencyCode); + Optional.ofNullable(sellScreenCryptoCurrencyCode).ifPresent(builder::setSellScreenCryptoCurrencyCode); + Optional.ofNullable(selectedPaymentAccountForCreateOffer).ifPresent( + account -> builder.setSelectedPaymentAccountForCreateOffer(selectedPaymentAccountForCreateOffer.toProtoMessage())); Optional.ofNullable(bridgeAddresses).ifPresent(builder::addAllBridgeAddresses); Optional.ofNullable(customBridges).ifPresent(builder::setCustomBridges); Optional.ofNullable(referralId).ifPresent(builder::setReferralId); @@ -249,6 +260,8 @@ public final class PreferencesPayload implements PersistableEnvelope { ProtoUtil.stringOrNullFromProto(proto.getTradeChartsScreenCurrencyCode()), ProtoUtil.stringOrNullFromProto(proto.getBuyScreenCurrencyCode()), ProtoUtil.stringOrNullFromProto(proto.getSellScreenCurrencyCode()), + ProtoUtil.stringOrNullFromProto(proto.getBuyScreenCryptoCurrencyCode()), + ProtoUtil.stringOrNullFromProto(proto.getSellScreenCryptoCurrencyCode()), proto.getTradeStatisticsTickUnitIndex(), proto.getResyncSpvRequested(), proto.getSortMarketCurrenciesNumerically(), @@ -278,9 +291,11 @@ public final class PreferencesPayload implements PersistableEnvelope { proto.getTakeOfferSelectedPaymentAccountId().isEmpty() ? null : proto.getTakeOfferSelectedPaymentAccountId(), proto.getBuyerSecurityDepositAsPercent(), proto.getIgnoreDustThreshold(), + proto.getClearDataAfterDays(), proto.getBuyerSecurityDepositAsPercentForCrypto(), proto.getBlockNotifyPort(), proto.getTacAcceptedV120(), + proto.getBsqAverageTrimThreshold(), proto.getAutoConfirmSettingsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAutoConfirmSettingsList().stream() .map(AutoConfirmSettings::fromProto) diff --git a/core/src/main/java/bisq/core/user/UserPayload.java b/core/src/main/java/bisq/core/user/UserPayload.java index dd87be54c3..31364d0c59 100644 --- a/core/src/main/java/bisq/core/user/UserPayload.java +++ b/core/src/main/java/bisq/core/user/UserPayload.java @@ -33,6 +33,7 @@ import bisq.common.proto.persistable.PersistableEnvelope; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -132,6 +133,7 @@ public class UserPayload implements PersistableEnvelope { ProtoUtil.stringOrNullFromProto(proto.getAccountId()), proto.getPaymentAccountsList().isEmpty() ? new HashSet<>() : new HashSet<>(proto.getPaymentAccountsList().stream() .map(e -> PaymentAccount.fromProto(e, coreProtoResolver)) + .filter(Objects::nonNull) .collect(Collectors.toSet())), proto.hasCurrentPaymentAccount() ? PaymentAccount.fromProto(proto.getCurrentPaymentAccount(), coreProtoResolver) : null, proto.getAcceptedLanguageLocaleCodesList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAcceptedLanguageLocaleCodesList()), diff --git a/core/src/main/java/bisq/core/util/AveragePriceUtil.java b/core/src/main/java/bisq/core/util/AveragePriceUtil.java new file mode 100644 index 0000000000..474e84d582 --- /dev/null +++ b/core/src/main/java/bisq/core/util/AveragePriceUtil.java @@ -0,0 +1,139 @@ +/* + * 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 . + */ + +package bisq.core.util; + +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.core.trade.statistics.TradeStatistics3; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.Preferences; + +import bisq.common.util.MathUtils; +import bisq.common.util.Tuple2; + +import org.bitcoinj.utils.Fiat; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Comparator; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.stream.Collectors; + +public class AveragePriceUtil { + private static final double HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER = 10; + + public static Tuple2 getAveragePriceTuple(Preferences preferences, + TradeStatisticsManager tradeStatisticsManager, + int days) { + double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100)); + Date pastXDays = getPastDate(days); + List bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .filter(e -> e.getCurrency().equals("BSQ")) + .filter(e -> e.getDate().after(pastXDays)) + .collect(Collectors.toList()); + List bsqTradePastXDays = percentToTrim > 0 ? + removeOutliers(bsqAllTradePastXDays, percentToTrim) : + bsqAllTradePastXDays; + + List usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .filter(e -> e.getCurrency().equals("USD")) + .filter(e -> e.getDate().after(pastXDays)) + .collect(Collectors.toList()); + List usdTradePastXDays = percentToTrim > 0 ? + removeOutliers(usdAllTradePastXDays, percentToTrim) : + usdAllTradePastXDays; + + Price usdPrice = Price.valueOf("USD", getUSDAverage(bsqTradePastXDays, usdTradePastXDays)); + Price bsqPrice = Price.valueOf("BSQ", getBTCAverage(bsqTradePastXDays)); + return new Tuple2<>(usdPrice, bsqPrice); + } + + private static List removeOutliers(List list, double percentToTrim) { + List yValues = list.stream() + .filter(TradeStatistics3::isValid) + .map(e -> (double) e.getPrice()) + .collect(Collectors.toList()); + + Tuple2 tuple = InlierUtil.findInlierRange(yValues, percentToTrim, HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER); + double lowerBound = tuple.first; + double upperBound = tuple.second; + return list.stream() + .filter(e -> e.getPrice() > lowerBound) + .filter(e -> e.getPrice() < upperBound) + .collect(Collectors.toList()); + } + + private static long getBTCAverage(List list) { + long accumulatedVolume = 0; + long accumulatedAmount = 0; + + for (TradeStatistics3 item : list) { + accumulatedVolume += item.getTradeVolume().getValue(); + accumulatedAmount += item.getTradeAmount().getValue(); // Amount of BTC traded + } + long averagePrice; + double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, Altcoin.SMALLEST_UNIT_EXPONENT); + averagePrice = accumulatedVolume > 0 ? MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / accumulatedVolume) : 0; + + return averagePrice; + } + + private static long getUSDAverage(List bsqList, List usdList) { + // Use next USD/BTC print as price to calculate BSQ/USD rate + // Store each trade as amount of USD and amount of BSQ traded + List> usdBsqList = new ArrayList<>(bsqList.size()); + usdList.sort(Comparator.comparing(TradeStatistics3::getDateAsLong)); + var usdBTCPrice = 10000d; // Default to 10000 USD per BTC if there is no USD feed at all + + for (TradeStatistics3 item : bsqList) { + // Find usdprice for trade item + usdBTCPrice = usdList.stream() + .filter(usd -> usd.getDateAsLong() > item.getDateAsLong()) + .map(usd -> MathUtils.scaleDownByPowerOf10((double) usd.getTradePrice().getValue(), + Fiat.SMALLEST_UNIT_EXPONENT)) + .findFirst() + .orElse(usdBTCPrice); + var bsqAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeVolume().getValue(), + Altcoin.SMALLEST_UNIT_EXPONENT); + var btcAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeAmount().getValue(), + Altcoin.SMALLEST_UNIT_EXPONENT); + usdBsqList.add(new Tuple2<>(usdBTCPrice * btcAmount, bsqAmount)); + } + long averagePrice; + var usdTraded = usdBsqList.stream() + .mapToDouble(item -> item.first) + .sum(); + var bsqTraded = usdBsqList.stream() + .mapToDouble(item -> item.second) + .sum(); + var averageAsDouble = bsqTraded > 0 ? usdTraded / bsqTraded : 0d; + var averageScaledUp = MathUtils.scaleUpByPowerOf10(averageAsDouble, Fiat.SMALLEST_UNIT_EXPONENT); + averagePrice = bsqTraded > 0 ? MathUtils.roundDoubleToLong(averageScaledUp) : 0; + + return averagePrice; + } + + private static Date getPastDate(int days) { + Calendar cal = new GregorianCalendar(); + cal.setTime(new Date()); + cal.add(Calendar.DAY_OF_MONTH, -1 * days); + return cal.getTime(); + } +} diff --git a/core/src/main/java/bisq/core/util/FeeReceiverSelector.java b/core/src/main/java/bisq/core/util/FeeReceiverSelector.java new file mode 100644 index 0000000000..c052d218c5 --- /dev/null +++ b/core/src/main/java/bisq/core/util/FeeReceiverSelector.java @@ -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 . + */ + +package bisq.core.util; + +import bisq.core.filter.FilterManager; + +import bisq.common.config.Config; + +import org.bitcoinj.core.Coin; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Random; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class FeeReceiverSelector { + public static final String BTC_FEE_RECEIVER_ADDRESS = "38bZBj5peYS3Husdz7AH3gEUiUbYRD951t"; + + public static String getMostRecentAddress() { + return Config.baseCurrencyNetwork().isMainnet() ? BTC_FEE_RECEIVER_ADDRESS : + Config.baseCurrencyNetwork().isTestnet() ? "2N4mVTpUZAnhm9phnxB7VrHB4aBhnWrcUrV" : + "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w"; + } + + public static String getAddress(FilterManager filterManager) { + return getAddress(filterManager, new Random()); + } + + @VisibleForTesting + static String getAddress(FilterManager filterManager, Random rnd) { + List feeReceivers = Optional.ofNullable(filterManager.getFilter()) + .flatMap(f -> Optional.ofNullable(f.getBtcFeeReceiverAddresses())) + .orElse(List.of()); + + List amountList = new ArrayList<>(); + List receiverAddressList = new ArrayList<>(); + + feeReceivers.forEach(e -> { + try { + String[] tokens = e.split("#"); + amountList.add(Coin.parseCoin(tokens[1]).longValue()); // total amount the victim should receive + receiverAddressList.add(tokens[0]); // victim's receiver address + } catch (RuntimeException ignore) { + // If input format is not as expected we ignore entry + } + }); + + if (!amountList.isEmpty()) { + return receiverAddressList.get(weightedSelection(amountList, rnd)); + } + + // If no fee address receiver is defined via filter we use the hard coded recent address + return getMostRecentAddress(); + } + + @VisibleForTesting + static int weightedSelection(List weights, Random rnd) { + long sum = weights.stream().mapToLong(n -> n).sum(); + long target = rnd.longs(0, sum).findFirst().orElseThrow(); + int i; + for (i = 0; i < weights.size() && target >= 0; i++) { + target -= weights.get(i); + } + return i - 1; + } +} diff --git a/core/src/main/java/bisq/core/util/FormattingUtils.java b/core/src/main/java/bisq/core/util/FormattingUtils.java index 3086e692e2..d6658e7129 100644 --- a/core/src/main/java/bisq/core/util/FormattingUtils.java +++ b/core/src/main/java/bisq/core/util/FormattingUtils.java @@ -127,7 +127,10 @@ public class FormattingUtils { try { // TODO quick hack... String res; - res = altcoinFormat.noCode().format(altcoin).toString(); + if (altcoin.getCurrencyCode().equals("BSQ")) + res = altcoinFormat.noCode().minDecimals(2).repeatOptionalDecimals(0, 0).format(altcoin).toString(); + else + res = altcoinFormat.noCode().format(altcoin).toString(); if (appendCurrencyCode) return res + " " + altcoin.getCurrencyCode(); else diff --git a/core/src/main/java/bisq/core/util/JsonUtil.java b/core/src/main/java/bisq/core/util/JsonUtil.java new file mode 100644 index 0000000000..1a8165bdfa --- /dev/null +++ b/core/src/main/java/bisq/core/util/JsonUtil.java @@ -0,0 +1,51 @@ +/* + * 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 . + */ + +package bisq.core.util; + +import bisq.common.util.JsonExclude; +import bisq.core.offer.OfferPayload; +import bisq.core.trade.Contract; +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.GsonBuilder; + + +public class JsonUtil { + public static String objectToJson(Object object) { + GsonBuilder gsonBuilder = new GsonBuilder() + .setExclusionStrategies(new AnnotationExclusionStrategy()) + .setPrettyPrinting(); + if (object instanceof Contract || object instanceof OfferPayload) { + gsonBuilder.registerTypeAdapter(OfferPayload.class, + new OfferPayload.JsonSerializer()); + } + return gsonBuilder.create().toJson(object); + } + + private static class AnnotationExclusionStrategy implements ExclusionStrategy { + @Override + public boolean shouldSkipField(FieldAttributes f) { + return f.getAnnotation(JsonExclude.class) != null; + } + + @Override + public boolean shouldSkipClass(Class clazz) { + return false; + } + } +} diff --git a/core/src/main/java/bisq/core/util/ParsingUtils.java b/core/src/main/java/bisq/core/util/ParsingUtils.java index f88fc65a15..d5172293c7 100644 --- a/core/src/main/java/bisq/core/util/ParsingUtils.java +++ b/core/src/main/java/bisq/core/util/ParsingUtils.java @@ -54,7 +54,7 @@ public class ParsingUtils { public static long atomicUnitsToCentineros(long atomicUnits) { return atomicUnits / CENTINEROS_AU_MULTIPLIER.longValue(); } - + public static Coin parseToCoin(String input, CoinFormatter coinFormatter) { return parseToCoin(input, coinFormatter.getMonetaryFormat()); } diff --git a/core/src/main/java/bisq/core/util/PriceUtil.java b/core/src/main/java/bisq/core/util/PriceUtil.java index b48c2fd60a..7813ff2575 100644 --- a/core/src/main/java/bisq/core/util/PriceUtil.java +++ b/core/src/main/java/bisq/core/util/PriceUtil.java @@ -17,21 +17,20 @@ package bisq.core.util; -import bisq.core.util.validation.AltcoinValidator; -import bisq.core.util.validation.FiatPriceValidator; -import bisq.core.util.validation.MonetaryValidator; - import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferDirection; import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.user.Preferences; +import bisq.core.util.validation.AltcoinValidator; +import bisq.core.util.validation.FiatPriceValidator; import bisq.core.util.validation.InputValidator; +import bisq.core.util.validation.MonetaryValidator; import bisq.common.util.MathUtils; @@ -39,9 +38,11 @@ import org.bitcoinj.utils.Fiat; import javax.inject.Inject; import javax.inject.Singleton; -import lombok.extern.slf4j.Slf4j; + import java.util.Optional; +import lombok.extern.slf4j.Slf4j; + import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @@ -63,7 +64,7 @@ public class PriceUtil { } public static InputValidator.ValidationResult isTriggerPriceValid(String triggerPriceAsString, - Price price, + MarketPrice marketPrice, boolean isSellOffer, boolean isFiatCurrency) { if (triggerPriceAsString == null || triggerPriceAsString.isEmpty()) { @@ -75,20 +76,21 @@ public class PriceUtil { return result; } - long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, price.getCurrencyCode()); - long priceAsLong = price.getValue(); - String priceAsString = FormattingUtils.formatPrice(price); + long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, marketPrice.getCurrencyCode()); + long marketPriceAsLong = PriceUtil.getMarketPriceAsLong("" + marketPrice.getPrice(), marketPrice.getCurrencyCode()); + String marketPriceAsString = FormattingUtils.formatMarketPrice(marketPrice.getPrice(), marketPrice.getCurrencyCode()); + if ((isSellOffer && isFiatCurrency) || (!isSellOffer && !isFiatCurrency)) { - if (triggerPriceAsLong >= priceAsLong) { + if (triggerPriceAsLong >= marketPriceAsLong) { return new InputValidator.ValidationResult(false, - Res.get("createOffer.triggerPrice.invalid.tooHigh", priceAsString)); + Res.get("createOffer.triggerPrice.invalid.tooHigh", marketPriceAsString)); } else { return new InputValidator.ValidationResult(true); } } else { - if (triggerPriceAsLong <= priceAsLong) { + if (triggerPriceAsLong <= marketPriceAsLong) { return new InputValidator.ValidationResult(false, - Res.get("createOffer.triggerPrice.invalid.tooLow", priceAsString)); + Res.get("createOffer.triggerPrice.invalid.tooLow", marketPriceAsString)); } else { return new InputValidator.ValidationResult(true); } @@ -115,14 +117,14 @@ public class PriceUtil { } public Optional getMarketBasedPrice(Offer offer, - OfferPayload.Direction direction) { + OfferDirection direction) { if (offer.isUseMarketBasedPrice()) { - return Optional.of(offer.getMarketPriceMargin()); + return Optional.of(offer.getMarketPriceMarginPct()); } if (!hasMarketPrice(offer)) { log.trace("We don't have a market price. " + - "That case could only happen if you don't have a price feed."); + "That case could only happen if you don't have a price feed."); return Optional.empty(); } @@ -133,9 +135,9 @@ public class PriceUtil { return calculatePercentage(offer, marketPriceAsDouble, direction); } - public Optional calculatePercentage(Offer offer, - double marketPrice, - OfferPayload.Direction direction) { + public static Optional calculatePercentage(Offer offer, + double marketPrice, + OfferDirection direction) { // If the offer did not use % price we calculate % from current market price String currencyCode = offer.getCurrencyCode(); Price price = offer.getPrice(); @@ -145,7 +147,7 @@ public class PriceUtil { long priceAsLong = checkNotNull(price).getValue(); double scaled = MathUtils.scaleDownByPowerOf10(priceAsLong, precision); double value; - if (direction == OfferPayload.Direction.SELL) { + if (direction == OfferDirection.SELL) { if (CurrencyUtil.isFiatCurrency(currencyCode)) { if (marketPrice == 0) { return Optional.empty(); diff --git a/core/src/main/java/bisq/core/util/SimpleMarkdownParser.java b/core/src/main/java/bisq/core/util/SimpleMarkdownParser.java new file mode 100644 index 0000000000..27341998cc --- /dev/null +++ b/core/src/main/java/bisq/core/util/SimpleMarkdownParser.java @@ -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 . + */ + +package bisq.core.util; + +import java.util.ArrayList; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +public class SimpleMarkdownParser { + private enum MarkdownParsingState { + TEXT, + LINK_TEXT, + LINK_HREF + } + + // Simple parser without correctness validation, currently supports only links + public static List parse(String markdown) { + List items = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + StringBuilder sb2 = new StringBuilder(); + + MarkdownParsingState state = MarkdownParsingState.TEXT; + + for (int i = 0; i < markdown.length(); i++) { + char c = markdown.charAt(i); + if (c == '[') { + if (sb.length() > 0) { + items.add(new TextNode(sb.toString())); + sb = new StringBuilder(); + } + state = MarkdownParsingState.LINK_TEXT; + } else if (c == '(' && state == MarkdownParsingState.LINK_TEXT) { + state = MarkdownParsingState.LINK_HREF; + } else if (c == ')' && state == MarkdownParsingState.LINK_HREF) { + state = MarkdownParsingState.TEXT; + items.add(new HyperlinkNode(sb.toString(), sb2.toString())); + sb = new StringBuilder(); + sb2 = new StringBuilder(); + } else if (c != ']') { + if (state == MarkdownParsingState.LINK_HREF) { + sb2.append(c); + } else { + sb.append(c); + } + } + } + if (sb.length() > 0) { + items.add(new TextNode(sb.toString())); + } + return items; + } + + public static class MarkdownNode {} + + @AllArgsConstructor + public static class HyperlinkNode extends MarkdownNode { + @Getter private final String text; + @Getter private final String href; + } + + @AllArgsConstructor + public static class TextNode extends MarkdownNode { + @Getter private final String text; + } +} diff --git a/core/src/main/java/bisq/core/util/VolumeUtil.java b/core/src/main/java/bisq/core/util/VolumeUtil.java index 08b5e65f4c..1c6808b913 100644 --- a/core/src/main/java/bisq/core/util/VolumeUtil.java +++ b/core/src/main/java/bisq/core/util/VolumeUtil.java @@ -17,17 +17,28 @@ package bisq.core.util; +import bisq.core.locale.Res; import bisq.core.monetary.Altcoin; import bisq.core.monetary.AltcoinExchangeRate; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; +import bisq.core.offer.Offer; import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Monetary; import org.bitcoinj.utils.ExchangeRate; import org.bitcoinj.utils.Fiat; +import org.bitcoinj.utils.MonetaryFormat; + +import java.text.DecimalFormat; +import java.text.NumberFormat; + +import java.util.Locale; public class VolumeUtil { + private static final MonetaryFormat FIAT_VOLUME_FORMAT = new MonetaryFormat().shift(0).minDecimals(0).repeatOptionalDecimals(0, 0); + public static Volume getRoundedFiatVolume(Volume volumeByAmount) { // We want to get rounded to 1 unit of the fiat currency, e.g. 1 EUR. return getAdjustedFiatVolume(volumeByAmount, 1); @@ -62,4 +73,76 @@ public class VolumeUtil { return new Volume(new ExchangeRate((Fiat) price.getMonetary()).coinToFiat(amount)); } } + + + public static String formatVolume(Offer offer, Boolean decimalAligned, int maxNumberOfDigits) { + return formatVolume(offer, decimalAligned, maxNumberOfDigits, true); + } + + public static String formatVolume(Offer offer, Boolean decimalAligned, int maxNumberOfDigits, boolean showRange) { + String formattedVolume = offer.isRange() && showRange + ? formatVolume(offer.getMinVolume()) + FormattingUtils.RANGE_SEPARATOR + formatVolume(offer.getVolume()) + : formatVolume(offer.getVolume()); + + if (decimalAligned) { + formattedVolume = FormattingUtils.fillUpPlacesWithEmptyStrings(formattedVolume, maxNumberOfDigits); + } + return formattedVolume; + } + + public static String formatLargeFiat(double value, String currency) { + if (value <= 0) { + return "0"; + } + NumberFormat numberFormat = DecimalFormat.getInstance(Locale.US); + numberFormat.setGroupingUsed(true); + return numberFormat.format(value) + " " + currency; + } + + public static String formatLargeFiatWithUnitPostFix(double value, String currency) { + if (value <= 0) { + return "0"; + } + String[] units = new String[]{"", "K", "M", "B"}; + int digitGroups = (int) (Math.log10(value) / Math.log10(1000)); + return new DecimalFormat("#,##0.###") + .format(value / Math.pow(1000, digitGroups)) + units[digitGroups] + " " + currency; + } + + public static String formatVolume(Volume volume) { + return formatVolume(volume, FIAT_VOLUME_FORMAT, false); + } + + private static String formatVolume(Volume volume, MonetaryFormat fiatVolumeFormat, boolean appendCurrencyCode) { + if (volume != null) { + Monetary monetary = volume.getMonetary(); + if (monetary instanceof Fiat) + return FormattingUtils.formatFiat((Fiat) monetary, fiatVolumeFormat, appendCurrencyCode); + else + return FormattingUtils.formatAltcoinVolume((Altcoin) monetary, appendCurrencyCode); + } else { + return ""; + } + } + + public static String formatVolumeWithCode(Volume volume) { + return formatVolume(volume, true); + } + + public static String formatVolume(Volume volume, boolean appendCode) { + return formatVolume(volume, FIAT_VOLUME_FORMAT, appendCode); + } + + public static String formatAverageVolumeWithCode(Volume volume) { + return formatVolume(volume, FIAT_VOLUME_FORMAT.minDecimals(2), true); + } + + public static String formatVolumeLabel(String currencyCode) { + return formatVolumeLabel(currencyCode, ""); + } + + public static String formatVolumeLabel(String currencyCode, String postFix) { + return Res.get("formatter.formatVolumeLabel", + currencyCode, postFix); + } } diff --git a/core/src/main/resources/bisq.policy b/core/src/main/resources/haveno.policy similarity index 100% rename from core/src/main/resources/bisq.policy rename to core/src/main/resources/haveno.policy diff --git a/core/src/main/resources/bisq.properties b/core/src/main/resources/haveno.properties similarity index 100% rename from core/src/main/resources/bisq.properties rename to core/src/main/resources/haveno.properties diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 35ea463a83..41d6293a32 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -28,11 +28,14 @@ shared.readMore=Read more shared.openHelp=Open Help shared.warning=Warning shared.close=Close +shared.closeAnywayDanger=Shut down anyway (DANGER!) +shared.okWait=Ok I'll wait shared.cancel=Cancel shared.ok=OK shared.yes=Yes shared.no=No shared.iUnderstand=I understand +shared.continueAnyway=Continue anyway shared.na=N/A shared.shutDown=Shut down shared.reportBug=Report bug on GitHub @@ -97,12 +100,12 @@ shared.BTCMinMax=XMR (min - max) shared.removeOffer=Remove offer shared.dontRemoveOffer=Don't remove offer shared.editOffer=Edit offer +shared.duplicateOffer=Duplicate offer shared.openLargeQRWindow=Open large QR code window -shared.tradingAccount=Trading account +shared.chooseTradingAccount=Choose trading account shared.faq=Visit FAQ page shared.yesCancel=Yes, cancel shared.nextStep=Next step -shared.selectTradingAccount=Select trading account shared.fundFromSavingsWalletButton=Transfer funds from Haveno wallet shared.fundFromExternalWalletButton=Open your external wallet for funding shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? @@ -128,6 +131,7 @@ shared.sendFundsDetailsWithFee=Sending: {0}\nFrom address: {1}\nTo receiving add # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copy to clipboard +shared.copiedToClipboard=Copied to clipboard! shared.language=Language shared.country=Country shared.applyAndShutDown=Apply and shut down @@ -195,6 +199,8 @@ shared.iConfirm=I confirm shared.openURL=Open {0} shared.fiat=Fiat shared.crypto=Crypto +shared.otherAssets=other assets +shared.other=Other shared.all=All shared.edit=Edit shared.advancedOptions=Advanced options @@ -218,6 +224,7 @@ shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transac shared.numItemsLabel=Number of entries: {0} shared.filter=Filter shared.enabled=Enabled +shared.me=Me #################################################################### @@ -229,8 +236,8 @@ shared.enabled=Enabled #################################################################### mainView.menu.market=Market -mainView.menu.buyBtc=Buy XMR -mainView.menu.sellBtc=Sell XMR +mainView.menu.buy=Buy +mainView.menu.sell=Sell mainView.menu.portfolio=Portfolio mainView.menu.funds=Funds mainView.menu.support=Support @@ -291,10 +298,6 @@ market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=Trades # OfferBookChartView -market.offerBook.buyAltcoin=Buy {0} (sell {1}) -market.offerBook.sellAltcoin=Sell {0} (buy {1}) -market.offerBook.buyWithFiat=Buy {0} -market.offerBook.sellWithFiat=Sell {0} market.offerBook.sellOffersHeaderLabel=Sell {0} to market.offerBook.buyOffersHeaderLabel=Buy {0} from market.offerBook.buy=I want to buy bitcoin @@ -326,20 +329,19 @@ market.trades.showVolumeInUSD=Show volume in USD offerbook.createOffer=Create offer offerbook.takeOffer=Take offer -offerbook.takeOfferToBuy=Take offer to buy {0} -offerbook.takeOfferToSell=Take offer to sell {0} +offerbook.takeOffer.createAccount=Create account and take offer offerbook.trader=Trader offerbook.offerersBankId=Maker''s bank ID (BIC/SWIFT): {0} offerbook.offerersBankName=Maker''s bank name: {0} offerbook.offerersBankSeat=Maker''s seat of bank country: {0} offerbook.offerersAcceptedBankSeatsEuro=Accepted seat of bank countries (taker): All Euro countries offerbook.offerersAcceptedBankSeats=Accepted seat of bank countries (taker):\n {0} -offerbook.availableOffers=Available offers -offerbook.filterByCurrency=Filter by currency -offerbook.filterByPaymentMethod=Filter by payment method +offerbook.availableOffersToBuy=Buy {0} with {1} +offerbook.availableOffersToSell=Sell {0} for {1} +offerbook.filterByCurrency=Choose currency +offerbook.filterByPaymentMethod=Choose payment method offerbook.matchingOffers=Offers matching my accounts offerbook.timeSinceSigning=Account info -offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted offerbook.timeSinceSigning.info.peerLimitLifted=signed by a peer and limits were lifted @@ -347,6 +349,14 @@ offerbook.timeSinceSigning.info.signer=signed by peer and can sign peer accounts offerbook.timeSinceSigning.info.banned=account was banned offerbook.timeSinceSigning.daysSinceSigning={0} days offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing +offerbook.timeSinceSigning.tooltip.accountLimit=Account limit: {0} +offerbook.timeSinceSigning.tooltip.accountLimitLifted=Account limit lifted +offerbook.timeSinceSigning.tooltip.info.unsigned=This account hasn't been signed yet +offerbook.timeSinceSigning.tooltip.info.signed=This account has been signed +offerbook.timeSinceSigning.tooltip.info.signedAndLifted=This account has been signed and can sign peer accounts +offerbook.timeSinceSigning.tooltip.checkmark.buyBtc=buy BTC from a signed account +offerbook.timeSinceSigning.tooltip.checkmark.wait=wait a minimum of {0} days +offerbook.timeSinceSigning.tooltip.learnMore=Learn more offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ @@ -354,8 +364,6 @@ offerbook.timeSinceSigning.help=When you successfully complete a trade with a pe offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} days offerbook.timeSinceSigning.notSigned.noNeed=N/A -shared.notSigned=This account has not been signed yet and was created {0} days ago -shared.notSigned.noNeed=This account type does not require signing shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago shared.notSigned.noNeedAlts=Altcoin accounts do not feature signing or aging @@ -364,33 +372,28 @@ offerbook.volume={0} (min - max) offerbook.deposit=Deposit BTC (%) offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. -offerbook.createOfferToBuy=Create new offer to buy {0} -offerbook.createOfferToSell=Create new offer to sell {0} -offerbook.createOfferToBuy.withFiat=Create new offer to buy {0} with {1} -offerbook.createOfferToSell.forFiat=Create new offer to sell {0} for {1} -offerbook.createOfferToBuy.withCrypto=Create new offer to sell {0} (buy {1}) -offerbook.createOfferToSell.forCrypto=Create new offer to buy {0} (sell {1}) +offerbook.createNewOffer=Create new offer to {0} {1} +offerbook.createOfferDisabled.tooltip=You can only create one offer at a time offerbook.takeOfferButton.tooltip=Take offer for {0} -offerbook.yesCreateOffer=Yes, create offer offerbook.setupNewAccount=Set up a new trading account offerbook.removeOffer.success=Remove offer was successful. offerbook.removeOffer.failed=Remove offer failed:\n{0} offerbook.deactivateOffer.failed=Deactivating of offer failed:\n{0} offerbook.activateOffer.failed=Publishing of offer failed:\n{0} -offerbook.withdrawFundsHint=You can withdraw the funds you paid in from the {0} screen. +offerbook.withdrawFundsHint=Offer has been removed. Funds are not reserved for this offer anymore. \ + You can send Available funds to an external wallet at the {0} screen. offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency -offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? +offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency. offerbook.warning.noMatchingAccount.headline=No matching payment account. offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due to counterparty trade restrictions -offerbook.warning.signatureNotValidated=This offer cannot be taken because its signature is not validated offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\n\ After successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\n\ - For more information on account signing, please see the documentation at [HYPERLINK:https://docs.bisq.network/payment-methods#account-signing]. + For more information on account signing, please see the documentation at [HYPERLINK:https://bisq.wiki/Account_limits#Account_signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n\ - The buyer''s account has not been signed by an arbitrator or a peer\n\ @@ -420,7 +423,6 @@ offerbook.info.sellAboveMarketPrice=You will get {0} more than the current marke offerbook.info.buyBelowMarketPrice=You will pay {0} less than the current market price (updated every minute). offerbook.info.buyAtFixedPrice=You will buy at this fixed price. offerbook.info.sellAtFixedPrice=You will sell at this fixed price. -offerbook.info.noArbitrationInUserLanguage=In case of a dispute, please note that arbitration for this offer will be handled in {0}. Language is currently set to {1}. offerbook.info.roundedFiatVolume=The amount was rounded to increase the privacy of your trade. #################################################################### @@ -431,8 +433,12 @@ createOffer.amount.prompt=Enter amount in XMR createOffer.price.prompt=Enter price createOffer.volume.prompt=Enter amount in {0} createOffer.amountPriceBox.amountDescription=Amount of XMR to {0} +createOffer.amountPriceBox.buy.amountDescriptionAltcoin=Amount in XMR to spend +createOffer.amountPriceBox.sell.amountDescriptionAltcoin=Amount in XMR to receive createOffer.amountPriceBox.buy.volumeDescription=Amount in {0} to spend createOffer.amountPriceBox.sell.volumeDescription=Amount in {0} to receive +createOffer.amountPriceBox.buy.volumeDescriptionAltcoin=Amount in {0} to sell +createOffer.amountPriceBox.sell.volumeDescriptionAltcoin=Amount in {0} to buy createOffer.amountPriceBox.minAmountDescription=Minimum amount of XMR createOffer.securityDeposit.prompt=Security deposit createOffer.fundsBox.title=Fund your offer @@ -461,6 +467,7 @@ createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries createOffer.placeOfferButton=Review: Place offer to {0} monero +createOffer.placeOfferButtonAltcoin=Review: Place offer to {0} {1} createOffer.createOfferFundWalletInfo.headline=Fund your offer # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Trade amount: {0} \n @@ -491,7 +498,6 @@ createOffer.useLowerValue=Yes, use my lower value createOffer.priceOutSideOfDeviation=The price you have entered is outside the max. allowed deviation from the market price.\nThe max. allowed deviation is {0} and can be adjusted in the preferences. createOffer.changePrice=Change price createOffer.tac=With publishing this offer I agree to trade with any trader who fulfills the conditions as defined in this screen. -createOffer.currencyForFee=Trade fee createOffer.setDeposit=Set buyer's security deposit (%) createOffer.setDepositAsBuyer=Set my security deposit as buyer (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) @@ -504,9 +510,11 @@ createOffer.minSecurityDepositUsed=Min. buyer security deposit is used # Offerbook / Take offer #################################################################### -takeOffer.amount.prompt=Enter amount in XMR +takeOffer.amount.prompt=Enter amount in BTC takeOffer.amountPriceBox.buy.amountDescription=Amount of XMR to sell takeOffer.amountPriceBox.sell.amountDescription=Amount of XMR to buy +takeOffer.amountPriceBox.buy.amountDescriptionAltcoin=Amount of XMR to spend +takeOffer.amountPriceBox.sell.amountDescriptionAltcoin=Amount of XMR to receive takeOffer.amountPriceBox.priceDescription=Price per bitcoin in {0} takeOffer.amountPriceBox.amountRangeDescription=Possible amount range takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces=The amount you have entered exceeds the number of allowed decimal places.\nThe amount has been adjusted to 4 decimal places. @@ -527,13 +535,13 @@ takeOffer.error.message=An error occurred when taking the offer.\n\n{0} # new entries takeOffer.takeOfferButton=Review: Take offer to {0} monero +takeOffer.takeOfferButtonAltcoin=Review: Take offer to {0} {1} takeOffer.noPriceFeedAvailable=You cannot take that offer as it uses a percentage price based on the market price but there is no price feed available. takeOffer.takeOfferFundWalletInfo.headline=Fund your trade # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Trade amount: {0} \n takeOffer.takeOfferFundWalletInfo.msg=You need to deposit {0} for taking this offer.\n\nThe amount is the sum of:\n{1}- Your security deposit: {2}\n- Trading fee: {3}\n- Total mining fees: {4}\n\nYou can choose between two options when funding your trade:\n- Use your Haveno wallet (convenient, but transactions may be linkable) OR\n- Transfer from an external wallet (potentially more private)\n\nYou will see all funding options and details after closing this popup. takeOffer.alreadyPaidInFunds=If you have already paid in funds you can withdraw it in the \"Funds/Send funds\" screen. -takeOffer.paymentInfo=Payment info takeOffer.setAmountPrice=Set amount takeOffer.alreadyFunded.askCancel=You have already funded that offer.\nIf you cancel now, your funds will be moved to your local Haveno wallet and are available for withdrawal in the \"Funds/Send funds\" screen.\nAre you sure you want to cancel? takeOffer.failed.offerNotAvailable=Take offer request failed because the offer is not available anymore. Maybe another trader has taken the offer in the meantime. @@ -645,29 +653,32 @@ portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, le # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=If your bank charges you any fees to make the transfer, you are responsible for paying those fees. # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.altcoin=Please transfer from your external {0} wallet\n{1} to the XMR seller.\n\n +portfolio.pending.step2_buyer.fees.swift=Make sure to use the SHA (shared fee model) to send the SWIFT payment. \ + See more details at [HYPERLINK:https://bisq.wiki/SWIFT#Use_the_correct_fee_option]. # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.cash=Please go to a bank and pay {0} to the XMR seller.\n\n -portfolio.pending.step2_buyer.cash.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment write on the paper receipt: NO REFUNDS.\nThen tear it in 2 parts, make a photo and send it to the XMR seller's email address. +portfolio.pending.step2_buyer.altcoin=Please transfer from your external {0} wallet\n{1} to the BTC seller.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.moneyGram=Please pay {0} to the XMR seller by using MoneyGram.\n\n -portfolio.pending.step2_buyer.moneyGram.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment send the Authorisation number and a photo of the receipt by email to the XMR seller.\n\ +portfolio.pending.step2_buyer.cash=Please go to a bank and pay {0} to the BTC seller.\n\n +portfolio.pending.step2_buyer.cash.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment write on the paper receipt: NO REFUNDS.\nThen tear it in 2 parts, make a photo and send it to the BTC seller's email address. +# suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.moneyGram=Please pay {0} to the BTC seller by using MoneyGram.\n\n +portfolio.pending.step2_buyer.moneyGram.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment send the Authorisation number and a photo of the receipt by email to the BTC seller.\n\ The receipt must clearly show the seller''s full name, country, state and the amount. The seller''s email is: {0}. # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.westernUnion=Please pay {0} to the XMR seller by using Western Union.\n\n -portfolio.pending.step2_buyer.westernUnion.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller.\n\ +portfolio.pending.step2_buyer.westernUnion=Please pay {0} to the BTC seller by using Western Union.\n\n +portfolio.pending.step2_buyer.westernUnion.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment send the MTCN (tracking number) and a photo of the receipt by email to the BTC seller.\n\ The receipt must clearly show the seller''s full name, city, country and the amount. The seller''s email is: {0}. # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.postal=Please send {0} by \"US Postal Money Order\" to the XMR seller.\n\n +portfolio.pending.step2_buyer.postal=Please send {0} by \"US Postal Money Order\" to the BTC seller.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.cashByMail=Please send {0} using \"Cash by Mail\" to the XMR seller. \ +portfolio.pending.step2_buyer.cashByMail=Please send {0} using \"Cash by Mail\" to the BTC seller. \ Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. \ - See more details about Cash by Mail on the Haveno wiki [HYPERLINK:https://bisq.wiki/Cash_by_Mail].\n\n + See more details about Cash by Mail on the Bisq wiki [HYPERLINK:https://bisq.wiki/Cash_by_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the BTC seller. You''ll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.f2f=Please contact the XMR seller by the provided contact and arrange a meeting to pay {0}.\n\n +portfolio.pending.step2_buyer.f2f=Please contact the BTC seller by the provided contact and arrange a meeting to pay {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Start payment using {0} portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} portfolio.pending.step2_buyer.amountToTransfer=Amount to transfer @@ -814,20 +825,15 @@ portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANT: By confirming you should delay confirmation of the payment as long as possible to reduce the risk of a chargeback. portfolio.pending.step5_buyer.groupTitle=Summary of completed trade +portfolio.pending.step5_buyer.groupTitle.mediated=This trade was resolved by mediation +portfolio.pending.step5_buyer.groupTitle.arbitrated=This trade was resolved by arbitration portfolio.pending.step5_buyer.tradeFee=Trade fee portfolio.pending.step5_buyer.makersMiningFee=Mining fee portfolio.pending.step5_buyer.takersMiningFee=Total mining fees portfolio.pending.step5_buyer.refunded=Refunded security deposit -portfolio.pending.step5_buyer.withdrawBTC=Withdraw your bitcoin (not applicable for XMR) -portfolio.pending.step5_buyer.amount=Amount to withdraw -portfolio.pending.step5_buyer.withdrawToAddress=Withdraw to address -portfolio.pending.step5_buyer.moveToHavenoWallet=Keep funds in Haveno wallet -portfolio.pending.step5_buyer.withdrawExternal=Withdraw to external wallet -portfolio.pending.step5_buyer.alreadyWithdrawn=Your funds have already been withdrawn.\nPlease check the transaction history. -portfolio.pending.step5_buyer.confirmWithdrawal=Confirm withdrawal request portfolio.pending.step5_buyer.amountTooLow=The amount to transfer is lower than the transaction fee and the min. possible tx value (dust). -portfolio.pending.step5_buyer.withdrawalCompleted.headline=Withdrawal completed -portfolio.pending.step5_buyer.withdrawalCompleted.msg=Your completed trades are stored under \"Portfolio/History\".\nYou can review all your bitcoin transactions under \"Funds/Transactions\" +portfolio.pending.step5_buyer.tradeCompleted.headline=Trade completed +portfolio.pending.step5_buyer.tradeCompleted.msg=Your completed trades are stored under \"Portfolio/History\".\nYou can review all your bitcoin transactions under \"Funds/Transactions\" portfolio.pending.step5_buyer.bought=You have bought portfolio.pending.step5_buyer.paid=You have paid @@ -849,6 +855,8 @@ portfolio.pending.tradePeriodInfo=After the first blockchain confirmation, the t portfolio.pending.tradePeriodWarning=If the period is exceeded both traders can open a dispute. portfolio.pending.tradeNotCompleted=Trade not completed in time (until {0}) portfolio.pending.tradeProcess=Trade process +portfolio.pending.stillNotResolved=If your issue remains unsolved, you can request support in our [Matrix chatroom](https://bisq.chat).\n\n\ + Also see [trading rules](https://bisq.wiki/Trading_rules) and [dispute resolution](https://bisq.wiki/Dispute_resolution) for reference. portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived \ (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask \ for additional help on the Haveno forum at [HYPERLINK:https://bisq.community]. @@ -871,9 +879,6 @@ portfolio.pending.error.depositTxNotConfirmed=The deposit transaction is not con For further help please contact the Haveno support channel at the Haveno Keybase team. portfolio.pending.support.headline.getHelp=Need help? -portfolio.pending.support.text.getHelp=If you have any problems you can try to contact the trade peer in the trade \ - chat or ask the Haveno community at https://bisq.community. \ - If your issue still isn't resolved, you can request more help from a mediator. portfolio.pending.support.button.getHelp=Open Trader Chat portfolio.pending.support.headline.halfPeriodOver=Check payment portfolio.pending.support.headline.periodOver=Trade period is over @@ -882,9 +887,9 @@ portfolio.pending.mediationRequested=Mediation requested portfolio.pending.refundRequested=Refund requested portfolio.pending.openSupport=Open support ticket portfolio.pending.supportTicketOpened=Support ticket opened -portfolio.pending.communicateWithArbitrator=Please communicate in the \"Support\" screen with the arbitrator. -portfolio.pending.communicateWithMediator=Please communicate in the \"Support\" screen with the mediator. -portfolio.pending.disputeOpenedMyUser=You opened already a dispute.\n{0} +portfolio.pending.communicateWithArbitrator=Please communicate with the arbitrator on the \"Support\" screen. +portfolio.pending.communicateWithMediator=Please communicate with the mediator on the \"Support\" screen. +portfolio.pending.disputeOpenedMyUser=You have already opened a dispute.\n{0} portfolio.pending.disputeOpenedByPeer=Your trading peer opened a dispute\n{0} portfolio.pending.noReceiverAddressDefined=No receiver address defined @@ -907,13 +912,13 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for \ exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion \ (or if the other peer is unresponsive).\n\n\ - More details about the new arbitration model: [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] + More details about the new arbitration model: [HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout \ but it seems that your trading peer has not accepted it.\n\n\ Once the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will \ investigate the case again and do a payout based on their findings.\n\n\ You can find more details about the arbitration model at:\ - [HYPERLINK:https://docs.bisq.network/trading-rules.html#arbitration] + [HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted @@ -933,7 +938,7 @@ portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=T but funds have been locked in the deposit transaction.\n\n\ Please do NOT send the fiat or altcoin payment to the XMR seller, because without the delayed payout tx, arbitration \ cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. \ - The mediator should suggest that both peers each get back the the full amount of their security deposits \ + The mediator should suggest that both peers each get back the full amount of their security deposits \ (with seller receiving full trade amount back as well). \ This way, there is no security risk, and only trade fees are lost. \n\n\ You can request a reimbursement for lost trade fees here: \ @@ -972,6 +977,7 @@ portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encou portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? +portfolio.failed.revertToPending.failed=Failed to move this trade to open trades. portfolio.failed.revertToPending=Move trade to open trades portfolio.closed.completed=Completed @@ -1098,6 +1104,7 @@ support.sigCheck.popup.failed=Signature verification failed support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenByTrader.failed=Failed to re-open the dispute. support.reOpenButton.label=Re-open support.sendNotificationButton.label=Private notification support.reportButton.label=Report @@ -1124,8 +1131,10 @@ support.closeTicket=Close ticket support.attachments=Attachments: support.savedInMailbox=Message saved in receiver's mailbox support.arrived=Message arrived at receiver +support.transient=Message is on its way to receiver support.acknowledged=Message arrival confirmed by receiver support.error=Receiver could not process message. Error: {0} +support.errorTimeout=timed out. Try sending message again. support.buyerAddress=XMR buyer address support.sellerAddress=XMR seller address support.role=Role @@ -1174,6 +1183,7 @@ support.peerOpenedTicket=Your trading peer has requested support due to technica support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorReceivedLogs=System message: Mediator has received logs: {0} support.mediatorsAddress=Mediator''s node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. \ It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. \ @@ -1683,7 +1693,10 @@ account.seed.restore.info=Please make a backup before applying restore from seed had many transactions. Please avoid interrupting that process, otherwise you might need to delete the SPV chain file \ again or repeat the restore process. account.seed.restore.ok=Ok, do the restore and shut down Haveno - +account.keys.clipboard.warning=Please note that wallet private keys are highly sensitive financial data.\n\n\ + ● You should NOT divulge any of your keys to any individual who asks for them, unless you are absolutely certain that they are to be trusted handling your money! \n\n\ + ● You should NOT copy private key data to the clipboard unless you are absolutely certain that you are running a secure computing environment that has no malware risks. \n\n\ + Many people have lost their Monero this way. If you have ANY doubts, close this dialog immediately and seek assistance from someone knowledgeable. #################################################################### # Mobile notifications @@ -1840,11 +1853,12 @@ disputeSummaryWindow.close.msg=Ticket closed on {0}\n\ Summary:\n\ Trade ID: {3}\n\ Currency: {4}\n\ - Trade amount: {5}\n\ - Payout amount for XMR buyer: {6}\n\ - Payout amount for XMR seller: {7}\n\n\ - Reason for dispute: {8}\n\n\ - Summary notes:\n{9}\n + Reason for dispute: {5}\n\ + Payout suggestion: {6}\n\ + Trade amount: {7}\n\ + Payout amount for XMR buyer: {8}\n\ + Payout amount for XMR seller: {9}\n\n\ + Summary notes:\n{10}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} @@ -1868,6 +1882,9 @@ disputeSummaryWindow.close.txDetails=Spending: {0}\n\ disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? +disputeSummaryWindow.close.alreadyPaid.headline=Payout already done +disputeSummaryWindow.close.alreadyPaid.text=Restart the client to do another payout for this dispute + emptyWalletWindow.headline={0} emergency wallet tool emptyWalletWindow.info=Please use that only in emergency case if you cannot access your fund from the UI.\n\n\ Please note that all open offers will be closed automatically when using this tool.\n\n\ @@ -1917,8 +1934,10 @@ offerDetailsWindow.countryBank=Maker's country of bank offerDetailsWindow.commitment=Commitment offerDetailsWindow.agree=I agree offerDetailsWindow.tac=Terms and conditions -offerDetailsWindow.confirm.maker=Confirm: Place offer to {0} monero -offerDetailsWindow.confirm.taker=Confirm: Take offer to {0} monero +offerDetailsWindow.confirm.maker=Confirm: Place offer to {0} bitcoin +offerDetailsWindow.confirm.makerAltcoin=Confirm: Place offer to {0} {1} +offerDetailsWindow.confirm.taker=Confirm: Take offer to {0} bitcoin +offerDetailsWindow.confirm.takerAltcoin=Confirm: Take offer to {0} {1} offerDetailsWindow.creationDate=Creation date offerDetailsWindow.makersOnion=Maker's onion address @@ -2016,10 +2035,7 @@ torNetworkSettingWindow.bridges.info=If Tor is blocked by your internet provider Visit the Tor web page at: https://bridges.torproject.org/bridges to learn more about \ bridges and pluggable transports. -feeOptionWindow.headline=Choose currency for trade fee payment -feeOptionWindow.info=You can choose to pay the trade fee in XMR. -feeOptionWindow.optionsLabel=Choose currency for trade fee payment -feeOptionWindow.useBTC=Use XMR +feeOptionWindow.useBTC=Use BTC feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.btcFeeWithPercentage={0} ({1}) @@ -2151,9 +2167,15 @@ popup.info.shutDownWithOpenOffers=Haveno is being shut down, but there are open they will be re-published to the P2P network the next time you start Haveno.\n\n\ To keep your offers online, keep Haveno running and make sure this computer remains online too \ (i.e., make sure it doesn't go into standby mode...monitor standby is not a problem). -popup.info.qubesOSSetupInfo=It appears you are running Haveno on Qubes OS. \n\n\ - Please make sure your Haveno qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes]. -popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Haveno version. +popup.info.shutDownWithTradeInit={0}\n\ + This trade has not finished initializing; shutting down now will probably make it corrupted. Please wait a minute and try again. +popup.info.qubesOSSetupInfo=It appears you are running Bisq on Qubes OS. \n\n\ + Please make sure your Bisq qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes]. +popup.info.firewallSetupInfo=It appears this machine blocks incoming Tor connections. \ + This can happen in VM environments such as Qubes/VirtualBox/Whonix. \n\n\ + Please set up your environment to accept incoming Tor connections, otherwise no-one will be able to take your offers. +popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Bisq version. +popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue. popup.privateNotification.headline=Important private notification! @@ -2317,6 +2339,10 @@ addressTextField.copyToClipboard=Copy address to clipboard addressTextField.addressCopiedToClipboard=Address has been copied to clipboard addressTextField.openWallet.failed=Opening a default Monero wallet application has failed. Perhaps you don't have one installed? +explorerAddressTextField.copyToClipboard=Copy address to clipboard +explorerAddressTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this address +explorerAddressTextField.missingTx.warning.tooltip=Missing required address + peerInfoIcon.tooltip={0}\nTag: {1} txIdTextField.copyIcon.tooltip=Copy transaction ID to clipboard @@ -2371,6 +2397,8 @@ XMR_STAGENET=Monero Stagenet time.year=Year time.month=Month +time.halfYear=Half-year +time.quarter=Quarter time.week=Week time.day=Day time.hour=Hour @@ -2397,7 +2425,6 @@ password.passwordsDoNotMatch=The 2 passwords you entered don't match. password.forgotPassword=Forgot password? password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\n\ It is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! -password.backupWasDone=I have already made a backup password.setPassword=Set Password (I already made a backup) password.makeBackup=Make Backup @@ -2441,6 +2468,7 @@ payment.account.owner=Account owner full name payment.account.fullName=Full name (first, middle, last) payment.account.state=State/Province/Region payment.account.city=City +payment.account.address=Address payment.bank.country=Country of bank payment.account.name.email=Account owner full name / email payment.account.name.emailAndHolderId=Account owner full name / email / {0} @@ -2466,6 +2494,45 @@ payment.select.altcoin=Select or search Altcoin payment.secret=Secret question payment.answer=Answer payment.wallet=Wallet ID +payment.capitual.cap=CAP Code +payment.upi.virtualPaymentAddress=Virtual Payment Address + +# suppress inspection "UnusedProperty" +payment.swift.headline=International SWIFT Wire Transfer +# suppress inspection "UnusedProperty" +payment.swift.title.bank=Receiving Bank +# suppress inspection "UnusedProperty" +payment.swift.title.intermediary=Intermediary Bank (click to expand) +# suppress inspection "UnusedProperty" +payment.swift.country.bank=Receiving Bank Country +# suppress inspection "UnusedProperty" +payment.swift.country.intermediary=Intermediary Bank Country +# suppress inspection "UnusedProperty" +payment.swift.swiftCode.bank=Receiving Bank SWIFT Code +# suppress inspection "UnusedProperty" +payment.swift.swiftCode.intermediary=Intermediary Bank SWIFT Code +# suppress inspection "UnusedProperty" +payment.swift.name.bank=Receiving Bank name +# suppress inspection "UnusedProperty" +payment.swift.name.intermediary=Intermediary Bank name +# suppress inspection "UnusedProperty" +payment.swift.branch.bank=Receiving Bank branch +# suppress inspection "UnusedProperty" +payment.swift.branch.intermediary=Intermediary Bank branch +# suppress inspection "UnusedProperty" +payment.swift.address.bank=Receiving Bank address +# suppress inspection "UnusedProperty" +payment.swift.address.intermediary=Intermediary Bank address +# suppress inspection "UnusedProperty" +payment.swift.address.beneficiary=Beneficiary address +# suppress inspection "UnusedProperty" +payment.swift.phone.beneficiary=Beneficiary phone number +payment.swift.account=Account No. (or IBAN) +payment.swift.use.intermediary=Use Intermediary Bank +payment.swift.showPaymentInfo=Show Payment Information... +payment.account.owner.address=Account owner address +payment.transferwiseUsd.address=(must be US-based, consider using bank address) + payment.amazon.site=Buy giftcard at payment.ask=Ask in Trader Chat payment.uphold.accountId=Username or email or phone no. @@ -2509,13 +2576,15 @@ payment.bankIdOptional=Bank ID (BIC/SWIFT) (optional) payment.branchNr=Branch no. payment.branchNrOptional=Branch no. (optional) payment.accountNrLabel=Account no. (IBAN) +payment.iban=IBAN +payment.tikkie.iban=IBAN used for Haveno trading on Tikkie payment.accountType=Account type payment.checking=Checking payment.savings=Savings payment.personalId=Personal ID payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\n\ It is highly recommended to either:\n\ -- make offers >{0}, so you only deal with signed/trusted buyers\n\ +- make offers >{0} XMR, so you only deal with signed/trusted buyers\n\ - keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\n\ Haveno developers are working on better ways to secure the payment account model for such smaller trades. \ Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. @@ -2597,12 +2666,273 @@ payment.account.amazonGiftCard.addCountryInfo={0}\n\ This will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\ +payment.swift.info.account=Carefully review the core guidelines for using SWIFT on Bisq:\n\ \n\ -- XMR buyers must write the BTC Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n\ -- XMR buyers must send the USPMO to the XMR seller with Delivery Confirmation.\n\ +- fill all fields completely and accurately \n\ +- buyer must send payment in currency specified by the offer maker \n\ +- buyer must use the shared fee model (SHA) \n\ +- buyer and seller may incur fees, so they should check their bank's fee schedules beforehand \n\ \n\ -In the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Haveno mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\n\ +SWIFT is more sophisticated than other payment methods, so please take a moment to review full guidance on the wiki [HYPERLINK:https://bisq.wiki/SWIFT]. + +payment.swift.info.buyer=To buy bitcoin with SWIFT, you must:\n\ +\n\ +- send payment in the currency specified by the offer maker \n\ +- use the shared fee model (SHA) to send payment\n\ +\n\ +Please review further guidance on the wiki to avoid penalties and ensure smooth trades [HYPERLINK:https://bisq.wiki/SWIFT]. + +payment.swift.info.seller=SWIFT senders are required to use the shared fee model (SHA) to send payments.\n\ +\n\ +If you receive a SWIFT payment that does not use SHA, open a mediation ticket.\n\ +\n\ +Please review further guidance on the wiki to avoid penalties and ensure smooth trades [HYPERLINK:https://bisq.wiki/SWIFT]. + +payment.imps.info.account=Please make sure to include your:\n\n\ + ● Account owner full name\n\ + ● Account number\n\ + ● IFSC number\n\n\ +These details should match your bank account that you will use for sending / receiving payments.\n\n\ +Please be aware there is a maximum of Rs. 200,000 that can be sent per transaction. If you are trading over this amount multiple transactions will be needed. However be aware their is a maximum limit of Rs. 1,000,000 that can be sent per day.\n\n\ +Some banks have different limits for their customers. +payment.imps.info.buyer=Please send payment only to the account details provided in Bisq.\n\n\ +The maximum trade size is Rs. 200,000 per transaction.\n\n\ +If your trade is over Rs. 200,000 you will have to make multiple transfers. However be aware their is a maximum limit of Rs. 1,000,000 that can be sent per day.\n\n\ +Please note some banks have different limits for their customers. +payment.imps.info.seller=If you intend to receive over Rs. 200,000 per trade you should expect the buyer to have to make multiple transfers. However be aware there is a maximum limit of Rs. 1,000,000 that can be sent per day.\n\n\ +Please note some banks have different limits for their customers. + +payment.neft.info.account=Please make sure to include your:\n\n\ + ● Account owner full name\n\ + ● Account number\n\ + ● IFSC number\n\n\ +These details should match your bank account that you will use for sending / receiving payments.\n\n\ +Please be aware there is a maximum of Rs. 50,000 that can be sent per transaction. If you are trading over this amount multiple transactions will be needed.\n\n\ +Please note some banks have different limits for their customers. +payment.neft.info.buyer=Please send payment only to the account details provided in Bisq.\n\n\ +The maximum trade size is Rs. 50,000 per transaction.\n\n\ +If your trade is over Rs. 50,000 you will have to make multiple transfers.\n\n\ +Please note some banks have different limits for their customers. +payment.neft.info.seller=If you intend to receive over Rs. 50,000 per trade you should expect the buyer to have to make multiple transfers.\n\n\ +Please note some banks have different limits for their customers. + +payment.paytm.info.account=Please make sure to include your email or phone number that matches your email or phone number in your PayTM account. \n\n\ +When users set up a PayTM account with No KYC users are limited to: \n\n\ + ● Maximum of Rs. 5,000 can be sent per transaction.\n\ + ● Maximum of Rs. 10,000 can be held in someone's PayTM wallet.\n\n\ +If you intend to trade amount of over 5,000 per trade you will need to complete KYC with PayTM. With KYC users are limited to:\n\n\ + ● Maximum of Rs. 100,000 can be sent per transaction.\n\ + ● Maximum of Rs. 100,000 can be held in someone's PayTM wallet.\n\n\ +Users should also be aware of account limits. Trades above PayTM account limits will likely have to take place over more than one day, or, be cancelled. +payment.paytm.info.buyer=Please send payment only to the email address or phone number provided.\n\n\ +If you intend to trade amount of over Rs. 5,000 per trade you will need to complete KYC with PayTM.\n\n\ +With No KYC Rs. 5,000 can be sent per transaction.\n\n\ +With KYC users Rs. 100,000 can be sent per transaction. +payment.paytm.info.seller=If you intend to trade amount of over Rs. 5,000 per trade you will need to complete KYC with PayTM. With KYC users are limited to:\n\n\ + ● Maximum of Rs. 100,000 can be sent per transaction.\n\ + ● Maximum of Rs. 100,000 can be held in your PayTM wallet .\n\n\ +Users should also be aware of account limits. As a maximum of Rs. 100,000 can be held in your PayTM wallet please make sure you transfer out your rupees regularly. + +payment.rtgs.info.account=RTGS is for payments of large trades of Rs. 200,000 or over.\n\n\ +When setting up your RTGS payment account please make sure to include your:\n\n\ + ● Account owner full name\n\ + ● Account number\n\ + ● IFSC number\n\n\ +These details should match your bank account that you will use for sending / receiving payments.\n\n\ +Please be aware there is a minimum trade amount of Rs. 200,000 that can be sent per transaction. If you are trading under this amount either the trade would get cancelled or both traders would have to agree on another payment method (eg IMPS or UPI). +payment.rtgs.info.buyer=Please send payment only to the account details provided in Bisq.\n\n\ +Please be aware there is a minimum trade amount of Rs. 200,000 that can be sent per transaction. If you are trading under this amount either the trade would get cancelled or both traders would have to agree on another payment method (eg IMPS or UPI). +payment.rtgs.info.seller=Please be aware there is a minimum trade amount of Rs. 200,000 that can be sent per transaction. If you are trading under this amount either the trade would get cancelled or both traders would have to agree on another payment method (eg IMPS or UPI). + +payment.upi.info.account=Please make sure to include your Virtual Payment Address (VPA) also called your UPI ID. The format for this is like an email ID: with the sign “@” in the middle. For example, your UPI ID could be “receiver’s_name@bank_name” or “phone_number@bank_name.” \n\n\ +For UPI there is a maximum limit of Rs. 100,000 that can be sent per transaction. \n\n\ +If you intend to trade amount of over Rs. 100,000 per trade it is likely trades will have to take place over multiple transfers. \n\n\ +Please note some banks have different limits for their customers. +payment.upi.info.buyer=Please send payment only to the VPA / UPI ID provided in Bisq. \n\n\ +The maximum trade size is Rs. 100,000 per transaction. \n\n\ +If your trade is over Rs. 100,000 you will have to make multiple transfers. \n\n\ +Please note some banks have different limits for their customers. +payment.upi.info.seller=If you intend to receive over Rs. 100,000 per trade you should expect the buyer to have to make multiple transfers. \n\n\ +Please note some banks have different limits for their customers. + +payment.celpay.info.account=Please make sure to include the email your Celsius account is registered to. \ + This will ensure that when you send funds they show from the correct account and when you receive funds they will be credited to your account.\n\n\ +CelPay users are limited to sending $2,500 (or other currency/crypto equivalent) in 24 hours.\n\n\ +Trades above CelPay account limits will likely have to take place over more than one day, or, be cancelled.\n\n\ +CelPay supports multiple stablecoins:\n\n\ + ● USD Stablecoins; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ + ● CAD Stablecoins; TrueCAD\n\ + ● GBP Stablecoins; TrueGBP\n\ + ● HKD Stablecoins; TrueHKD\n\ + ● AUD Stablecoins; TrueAUD\n\n\ +BTC Buyers can send any matching currency stablecoin to the BTC Seller. +payment.celpay.info.buyer=Please send payment only to the email address provided by the BTC Seller by sending a payment link.\n\n\ +CelPay is limited to sending $2,500 (or other currency/crypto equivalent) in 24 hours.\n\n\ +Trades above CelPay account limits will likely have to take place over more than one day, or, be cancelled.\n\n\ +CelPay supports multiple stablecoins:\n\n\ + ● USD Stablecoins; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ + ● CAD Stablecoins; TrueCAD\n\ + ● GBP Stablecoins; TrueGBP\n\ + ● HKD Stablecoins; TrueHKD\n\ + ● AUD Stablecoins; TrueAUD\n\n\ +BTC Buyers can send any matching currency stablecoin to the BTC Seller. +payment.celpay.info.seller=BTC Sellers should expect to receive payment via a secure payment link. \ + Please make sure the email payment link contains the email address provided by the BTC Buyer.\n\n\ +CelPay users are limited to sending $2,500 (or other currency/crypto equivalent) in 24 hours.\n\n\ +Trades above CelPay account limits will likely have to take place over more than one day, or, be cancelled.\n\n\ +CelPay supports multiple stablecoins:\n\n\ + ● USD Stablecoins; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ + ● CAD Stablecoins; TrueCAD\n\ + ● GBP Stablecoins; TrueGBP\n\ + ● HKD Stablecoins; TrueHKD\n\ + ● AUD Stablecoins; TrueAUD\n\n\ +BTC Sellers should expect to receive any matching currency stablecoin from the BTC Buyer. It is possible for the BTC Buyer to send any matching currency stablecoin. +payment.celpay.supportedCurrenciesForReceiver=Supported currencies (please note: all the currencies below are supported stable coins within the Celcius app. Trades are for stable coins, not fiat.) + +payment.nequi.info.account=Please make sure to include your phone number that is associated with your Nequi account.\n\n\ +When users set up a Nequi account payment limits are set to a maximum of ~ 7,000,000 COP that can be sent per month.\n\n\ +If you intend to trade amount of over 7,000,000 COP per trade you will need to complete KYC with Bancolombia and pay a fee \ + of around 15,000 COP. After this all transactions will incur a 0.4% of tax. Please ensure you are aware of the latest taxes.\n\n\ +Users should also be aware of account limits. If you trade over the above limits your trade might be cancelled and there could be a penalty. +payment.nequi.info.buyer=Please send payment only to the phone number provided in the BTC Seller's Bisq account.\n\n\ +When users set up a Nequi account, payment limits are set to a maximum of ~ 7,000,000 COP that can be sent per month.\n\n\ +If you intend to trade amount of over 7,000,000 COP per trade you will need to complete KYC with Bancolombia and pay a fee \ + of around 15,000 COP. After this all transactions will incur a 0.4% of tax. Please ensure you are aware of the latest taxes.\n\n\ +Users should also be aware of account limits. If you trade over the above limits your trade might be cancelled and there could be a penalty. +payment.nequi.info.seller=Please check that the payment received matches the phone number provided in the BTC Buyer's Bisq account.\n\n\ +When users set up a Nequi account, payment limits are set to a maximum of ~ 7,000,000 COP that can be sent per month.\n\n\ +If you intend to trade amount of over 7,000,000 COP per trade you will need to complete KYC with Bancolombia and pay a fee \ + of around 15,000 COP. After this all transactions will incur a 0.4% of tax. Please ensure you are aware of the latest taxes.\n\n\ +Users should also be aware of account limits. If you trade over the above limits your trade might be cancelled and there could be a penalty. + +payment.bizum.info.account=To use Bizum you need a bank account (IBAN) in Spain and to be registered for the service.\n\n\ +Bizum can be used for trades between €0.50 and €1,000.\n\n\ +The maximum amount of transactions you can send/receive using Bizum is €2,000 Euros per day.\n\n\ +Bizum users can have a maximum of 150 operations per month.\n\n\ +Each bank however may establish its own limits, within the above limits, for its clients.\n\n\ +Users should also be aware of account limits. If you trade over the above limits your trade might be cancelled and there could be a penalty. +payment.bizum.info.buyer=Please send payment only to the BTC Seller's mobile phone number as provided in Bisq.\n\n\ +The maximum trade size is €1,000 per payment. The maximum amount of transactions you can send using Bizum is €2,000 Euros per day.\n\n\ +If you trade over the above limits your trade might be cancelled and there could be a penalty. +payment.bizum.info.seller=Please make sure your payment is received from the BTC Buyer's mobile phone number as provided in Bisq.\n\n\ +The maximum trade size is €1,000 per payment. The maximum amount of transactions you can receive using Bizum is €2,000 Euros per day.\n\n\ +If you trade over the above limits your trade might be cancelled and there could be a penalty. + +payment.pix.info.account=Please make sure to include your chosen Pix Key. There are four types of keys: \ + CPF (Natural Persons Register) or CNPJ (National Registry of Legal Entities), e-mail address, telephone number or a \ + random key generated by the system called a universally unique identifier (UUID). A different key must be used for \ + each Pix account you have. Individuals can create up to five keys for each account they own.\n\n\ +When trading on Bisq, BTC Buyers should use their Pix Keys as the payment description so that it is easy for the BTC Sellers to identify the payment as coming from themselves. +payment.pix.info.buyer=Please send payment only the Pix Key provided in the BTC Seller's Bisq account.\n\n\ +Please use your Pix Key as the payment reference so that it is easy for the BTC Seller to identify the payment as coming from yourself. +payment.pix.info.seller=Please check that the payment received description matches the Pix Key provided in the BTC Buyer's Bisq account. +payment.pix.key=Pix Key (CPF, CNPJ, Email, Phone number or UUID) + +payment.monese.info.account=Monese is a bank app for users of GBP, EUR and RON*. Monese allows users to send money to \ + other Monese accounts instantly and for free in any supported currency.\n\n\ +*To open a RON account in Monese, you need to either live in Romania or have Romanian citizenship.\n\n\ +When setting up your Monese account in Bisq please make sure to include your name and phone number that matches your \ + Monese account. This will ensure that when you send funds they show from the correct account and when you receive \ + funds they will be credited to your account. +payment.monese.info.buyer=Please send payment only to the phone number provided by the BTC Seller in their Bisq account. Please leave the payment description blank. +payment.monese.info.seller=BTC Sellers should expect to receive payment from the phone number / name shown in the BTC Buyer's Bisq account. + +payment.satispay.info.account=To use Satispay you need a bank account (IBAN) in Italy and to be registered for the service.\n\n\ +Satispay account limits are individually set. If you want to trade increased amounts you will need to contact Satispay \ + support to increase your limits. Users should also be aware of account limits. If you trade over the above limits \ + your trade might be cancelled and there could be a penalty. +payment.satispay.info.buyer=Please send payment only to the BTC Seller's mobile phone number as provided in Bisq.\n\n\ +Satispay account limits are individually set. If you want to trade increased amounts you will need to contact Satispay \ + support to increase your limits. Users should also be aware of account limits. If you trade over the above limits \ + your trade might be cancelled and there could be a penalty. +payment.satispay.info.seller=Please make sure your payment is received from the BTC Buyer's mobile phone number / name as provided in Bisq.\n\n\ +Satispay account limits are individually set. If you want to trade increased amounts you will need to contact Satispay \ + support to increase your limits. Users should also be aware of account limits. If you trade over the above limits \ + your trade might be cancelled and there could be a penalty. + +payment.tikkie.info.account=To use Tikkie you need a bank account (IBAN) in The Netherlands and to be registered for the service.\n\n\ +When you send a Tikkie payment request to an individual person you can ask to receive a maximum of €750 per Tikkie \ + request. The maximum amount you can request within 24 hours is €2,500 per Tikkie account.\n\n\ +Each bank however may establish its own limits, within these limits, for its clients.\n\n\ +Users should also be aware of account limits. If you trade over the above limits your trade might be cancelled and there could be a penalty. +payment.tikkie.info.buyer=Please request a payment link from the BTC Seller in trader chat. Once the BTC Seller has \ + sent you a payment link that matches the correct amount for the trade please proceed to payment.\n\n\ +When the BTC Seller requests a Tikkie payment the maximum they can ask to receive is €750 per Tikkie request. If the \ + trade is over that amount the BTC Seller will have to sent multiple requests to total the trade amount. The maximum \ + you can request in a day is €2,500.\n\n\ +Each bank however may establish its own limits, within these limits, for its clients.\n\n\ +Users should also be aware of account limits. If you trade over the above limits your trade might be cancelled and there could be a penalty. +payment.tikkie.info.seller=Please send a payment link to the BTC Seller in trader chat. Once the BTC \ + Buyer has sent you payment please check their IBAN detail match the details they have in Bisq.\n\n\ +When the BTC Seller requests a Tikkie payment the maximum they can ask to receive is €750 per Tikkie request. If the \ + trade is over that amount the BTC Seller will have to sent multiple requests to total the trade amount. The maximum \ + you can request in a day is €2,500.\n\n\ +Each bank however may establish its own limits, within these limits, for its clients.\n\n\ +Users should also be aware of account limits. If you trade over the above limits your trade might be cancelled and there could be a penalty. + +payment.verse.info.account=Verse is a multiple currency payment method that can send and receive payment in EUR, SEK, HUF, DKK, PLN.\n\n\ +When setting up your Verse account in Bisq please make sure to include the username that matches your username in your \ + Verse account. This will ensure that when you send funds they show from the correct account and when you receive \ + funds they will be credited to your account.\n\n\ +Verse users are limited to sending or receiving €10,000 per year (or equivalent foreign currency amount) for \ + accumulated payments made from or received into their payment account. This can be increased by Verse on request. +payment.verse.info.buyer=Please send payment only to the username provided by the BTC Seller in their Bisq account. \ + Please leave the payment description blank.\n\n\ +Verse users are limited to sending or receiving €10,000 per year (or equivalent foreign currency amount) for \ + accumulated payments made from or received into their payment account. This can be increased by Verse on request. +payment.verse.info.seller=BTC Sellers should expect to receive payment from the username shown in the BTC Buyer's Bisq account.\n\n\ +Verse users are limited to sending or receiving €10,000 per year (or equivalent foreign currency amount) for \ + accumulated payments made from or received into their payment account. This can be increased by Verse on request. + +payment.achTransfer.info.account=When adding ACH as a payment method in Bisq users should make sure they are aware what \ + it will cost to send and receive an ACH transfer. +payment.achTransfer.info.buyer=Please ensure you are aware of what it will cost you to send an ACH transfer.\n\n\ + When paying, send only to the payment details provided in the BTC Seller's account using ACH transfer. +payment.achTransfer.info.seller=Please ensure you are aware of what it will cost you to receive an ACH transfer.\n\n\ + When receiving payment, please check that it is received from the BTC Buyer's account as an ACH transfer. + +payment.domesticWire.info.account=When adding Domestic Wire Transfer as a payment method in Bisq users should make sure \ + they are aware what it will cost to send and receive a wire transfer. +payment.domesticWire.info.buyer=Please ensure you are aware of what it will cost you to send a wire transfer.\n\n\ + When paying, send only to the payment details provided in the BTC Seller's account. +payment.domesticWire.info.seller=Please ensure you are aware of what it will cost you to receive a wire transfer.\n\n\ + When receiving payment, please check that it is received from the BTC Buyer's account. + +payment.strike.info.account=Please make sure to include your Strike username.\n\n\ +In Bisq, Strike is used for fiat to fiat payments only.\n\n\ +Please make sure you are aware of the Strike limits:\n\n\ +Users who have registered with only their email, name, and phone number have the following limits:\n\n\ + ● $100 maximum per deposit\n\ + ● $1,000 maximum total deposits per week\n\ + ● $100 maximum per payment\n\n\ +Users can increase their limits by providing Strike with more information. These users have the following limits:\n\n\ + ● $1,000 maximum per deposit\n\ + ● $1,000 maximum total deposits per week\n\ + ● $1,000 maximum per payment\n\n\ +If you trade over the above limits your trade might be cancelled and there could be a penalty. +payment.strike.info.buyer=Please send payment only to the BTC Seller's Strike username as provided in Bisq.\n\n\ +The maximum trade size is $1,000 per payment.\n\n\ +If you trade over the above limits your trade might be cancelled and there could be a penalty. +payment.strike.info.seller=Please make sure your payment is received from the BTC Buyer's Strike username as provided in Bisq.\n\n\ +The maximum trade size is $1,000 per payment.\n\n\ +If you trade over the above limits your trade might be cancelled and there could be a penalty. + +payment.transferwiseUsd.info.account=Due US banking regulation, sending and receiving USD payments has more restrictions \ + than most other currencies. For this reason USD was not added to Bisq TransferWise payment method.\n\n\ +The TransferWise-USD payment method allows Bisq users to trade in USD.\n\n\ +Anyone with a Wise, formally TransferWise account, can add TransferWise-USD as a payment method in Bisq. This will \ + allow them to buy and sell BTC with USD.\n\n\ +When trading on Bisq BTC Buyers should not use any reference for reason for payment. If reason for payment is required \ + they should only use the full name of the TransferWise-USD account owner. +payment.transferwiseUsd.info.buyer=Please send payment only to the email address in the BTC Seller's Bisq TransferWise-USD account. +payment.transferwiseUsd.info.seller=Please check that the payment received matches the BTC Buyer's name of the TransferWise-USD account in Bisq. + +payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\ +\n\ +- BTC buyers must write the BTC Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n\ +- BTC buyers must send the USPMO to the BTC seller with Delivery Confirmation.\n\ +\n\ +In the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Bisq mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\n\ Failure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\n\ In all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\n\ If you do not understand these requirements, do not trade using USPMO on Haveno. @@ -2656,10 +2986,12 @@ payment.f2f.info='Face to Face' trades have different rules and come with differ of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to \ an agreement.\n\n\ To be sure you fully understand the differences with 'Face to Face' trades please read the instructions and \ - recommendations at: [HYPERLINK:https://docs.bisq.network/trading-rules.html#f2f-trading] + recommendations at: [HYPERLINK:https://bisq.wiki/Face-to-face_(payment_method)] payment.f2f.info.openURL=Open web page payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} payment.f2f.offerbook.tooltip.extra=Additional information: {0} +payment.ifsc=IFS Code +payment.ifsc.validation=IFSC format: XXXX0999999 payment.japan.bank=Bank payment.japan.branch=Branch @@ -2671,15 +3003,13 @@ payment.payid.info=A PayID like a phone number, email address or an Australian B bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. \ Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\n\ - Haveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift \ - card''s message field. Please see the wiki [HYPERLINK:https://bisq.wiki/Amazon_eGift_card] for further details and best practices. \n\n\ + Please see the wiki [HYPERLINK:https://bisq.wiki/Amazon_eGift_card] for further details and best practices. \n\n\ Three important notes:\n\ - try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n\ - try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat \ to tell your trading peer the reference text you picked so they can verify your payment)\n\ - Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) - # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ @@ -2759,9 +3089,53 @@ ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=TransferWise # suppress inspection "UnusedProperty" +TRANSFERWISE_USD=TransferWise-USD +# suppress inspection "UnusedProperty" +PAYSERA=Paysera +# suppress inspection "UnusedProperty" +PAXUM=Paxum +# suppress inspection "UnusedProperty" +NEFT=India/NEFT +# suppress inspection "UnusedProperty" +RTGS=India/RTGS +# suppress inspection "UnusedProperty" +IMPS=India/IMPS +# suppress inspection "UnusedProperty" +UPI=India/UPI +# suppress inspection "UnusedProperty" +PAYTM=India/PayTM +# suppress inspection "UnusedProperty" +NEQUI=Nequi +# suppress inspection "UnusedProperty" +BIZUM=Bizum +# suppress inspection "UnusedProperty" +PIX=Pix +# suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Altcoins Instant +# suppress inspection "UnusedProperty" +CAPITUAL=Capitual +# suppress inspection "UnusedProperty" +CELPAY=CelPay +# suppress inspection "UnusedProperty" +MONESE=Monese +# suppress inspection "UnusedProperty" +SATISPAY=Satispay +# suppress inspection "UnusedProperty" +TIKKIE=Tikkie +# suppress inspection "UnusedProperty" +VERSE=Verse +# suppress inspection "UnusedProperty" +STRIKE=Strike +# suppress inspection "UnusedProperty" +SWIFT=SWIFT International Wire Transfer +# suppress inspection "UnusedProperty" +ACH_TRANSFER=ACH Transfer +# suppress inspection "UnusedProperty" +DOMESTIC_WIRE_TRANSFER=Domestic Wire Transfer +# suppress inspection "UnusedProperty" +BSQ_SWAP=BSQ Swap # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" @@ -2811,9 +3185,53 @@ ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=TransferWise # suppress inspection "UnusedProperty" +TRANSFERWISE_USD_SHORT=TransferWise-USD +# suppress inspection "UnusedProperty" +PAYSERA_SHORT=Paysera +# suppress inspection "UnusedProperty" +PAXUM_SHORT=Paxum +# suppress inspection "UnusedProperty" +NEFT_SHORT=NEFT +# suppress inspection "UnusedProperty" +RTGS_SHORT=RTGS +# suppress inspection "UnusedProperty" +IMPS_SHORT=IMPS +# suppress inspection "UnusedProperty" +UPI_SHORT=UPI +# suppress inspection "UnusedProperty" +PAYTM_SHORT=PayTM +# suppress inspection "UnusedProperty" +NEQUI_SHORT=Nequi +# suppress inspection "UnusedProperty" +BIZUM_SHORT=Bizum +# suppress inspection "UnusedProperty" +PIX_SHORT=Pix +# suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Altcoins Instant +# suppress inspection "UnusedProperty" +CAPITUAL_SHORT=Capitual +# suppress inspection "UnusedProperty" +CELPAY_SHORT=CelPay +# suppress inspection "UnusedProperty" +MONESE_SHORT=Monese +# suppress inspection "UnusedProperty" +SATISPAY_SHORT=Satispay +# suppress inspection "UnusedProperty" +TIKKIE_SHORT=Tikkie +# suppress inspection "UnusedProperty" +VERSE_SHORT=Verse +# suppress inspection "UnusedProperty" +STRIKE_SHORT=Strike +# suppress inspection "UnusedProperty" +SWIFT_SHORT=SWIFT +# suppress inspection "UnusedProperty" +ACH_TRANSFER_SHORT=ACH +# suppress inspection "UnusedProperty" +DOMESTIC_WIRE_TRANSFER_SHORT=Domestic Wire +# suppress inspection "UnusedProperty" +BSQ_SWAP_SHORT=BSQ Swap # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" @@ -2848,7 +3266,9 @@ validation.accountNrChars=Account number must consist of {0} characters. validation.btc.invalidAddress=The address is not correct. Please check the address format. validation.integerOnly=Please enter integer numbers only. validation.inputError=Your input caused an error:\n{0} +validation.bsq.insufficientBalance=Your available balance is {0}. validation.btc.exceedsMaxTradeLimit=Your trade limit is {0}. +validation.bsq.amountBelowMinAmount=Min. amount is {0} validation.nationalAccountId={0} must consist of {1} numbers. #new @@ -2876,6 +3296,7 @@ validation.iban.checkSumNotNumeric=Checksum must be numeric validation.iban.nonNumericChars=Non-alphanumeric character detected validation.iban.checkSumInvalid=IBAN checksum is invalid validation.iban.invalidLength=Number must have a length of 15 to 34 chars. +validation.iban.sepaNotSupported=SEPA is not supported in this country validation.interacETransfer.invalidAreaCode=Non-Canadian area code validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address validation.interacETransfer.invalidQuestion=Must contain only letters, numbers, spaces and/or the symbols ' _ , . ? - @@ -2901,3 +3322,5 @@ validation.phone.tooManyDigits=There are too many digits in {0} to be a valid ph validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. \ The correct dialing code is {2}. validation.invalidAddressList=Must be comma separated list of valid addresses +validation.capitual.invalidFormat=Must be a valid CAP code of format: CAP-XXXXXX (6 alphanumeric characters) + diff --git a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java index 0b898e9d3d..210bb0ec9d 100644 --- a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java @@ -65,7 +65,7 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; -import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById; +import static bisq.core.payment.payload.PaymentMethod.getPaymentMethod; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -237,7 +237,7 @@ public class AccountAgeWitnessServiceTest { when(contract.isBuyerMakerAndSellerTaker()).thenReturn(false); assertEquals(disputes.get(0).getBuyerPaymentAccountPayload(), buyerPaymentAccountPayload); assertEquals(disputes.get(0).getSellerPaymentAccountPayload(), sellerPaymentAccountPayload); - List items = service.getTraderPaymentAccounts(now, getPaymentMethodById(PaymentMethod.SEPA_ID), disputes); + List items = service.getTraderPaymentAccounts(now, getPaymentMethod(PaymentMethod.SEPA_ID), disputes); assertEquals(2, items.size()); // Setup a mocked arbitrator key diff --git a/core/src/test/java/bisq/core/offer/OfferMaker.java b/core/src/test/java/bisq/core/offer/OfferMaker.java index 1dc496b885..ce7972d236 100644 --- a/core/src/test/java/bisq/core/offer/OfferMaker.java +++ b/core/src/test/java/bisq/core/offer/OfferMaker.java @@ -30,7 +30,7 @@ public class OfferMaker { public static final Property amount = new Property<>(); public static final Property baseCurrencyCode = new Property<>(); public static final Property counterCurrencyCode = new Property<>(); - public static final Property direction = new Property<>(); + public static final Property direction = new Property<>(); public static final Property useMarketBasedPrice = new Property<>(); public static final Property marketPriceMargin = new Property<>(); public static final Property id = new Property<>(); @@ -40,7 +40,7 @@ public class OfferMaker { 0L, null, null, - lookup.valueOf(direction, OfferPayload.Direction.BUY), + lookup.valueOf(direction, OfferDirection.BUY), lookup.valueOf(price, 100000L), lookup.valueOf(marketPriceMargin, 0.0), lookup.valueOf(useMarketBasedPrice, false), diff --git a/core/src/test/java/bisq/core/util/ProtoUtilTest.java b/core/src/test/java/bisq/core/util/ProtoUtilTest.java index 5369bfa0eb..089bdc04ca 100644 --- a/core/src/test/java/bisq/core/util/ProtoUtilTest.java +++ b/core/src/test/java/bisq/core/util/ProtoUtilTest.java @@ -17,6 +17,7 @@ package bisq.core.util; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OpenOffer; import bisq.common.proto.ProtoUtil; @@ -34,10 +35,10 @@ public class ProtoUtilTest { //TODO Use NetworkProtoResolver, PersistenceProtoResolver or ProtoResolver which are all in bisq.common. @Test public void testEnum() { - OfferPayload.Direction direction = OfferPayload.Direction.SELL; - OfferPayload.Direction direction2 = OfferPayload.Direction.BUY; - OfferPayload.Direction realDirection = getDirection(direction); - OfferPayload.Direction realDirection2 = getDirection(direction2); + OfferDirection direction = OfferDirection.SELL; + OfferDirection direction2 = OfferDirection.BUY; + OfferDirection realDirection = getDirection(direction); + OfferDirection realDirection2 = getDirection(direction2); assertEquals("SELL", realDirection.name()); assertEquals("BUY", realDirection2.name()); } @@ -63,7 +64,7 @@ public class ProtoUtilTest { } } - public static OfferPayload.Direction getDirection(OfferPayload.Direction direction) { - return OfferPayload.Direction.valueOf(direction.name()); + public static OfferDirection getDirection(OfferDirection direction) { + return OfferDirection.valueOf(direction.name()); } } diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcMoneroConnectionsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcMoneroConnectionsService.java index e3c6c085eb..2a21dc27a9 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcMoneroConnectionsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcMoneroConnectionsService.java @@ -1,18 +1,18 @@ /* - * This file is part of Bisq. + * This file is part of Haveno. * - * Bisq is free software: you can redistribute it and/or modify it + * 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. * - * Bisq is distributed in the hope that it will be useful, but WITHOUT + * 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 Bisq. If not, see . + * along with Haveno. If not, see . */ package bisq.daemon.grpc; diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java index ce043825bc..c16c444ce6 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java @@ -47,7 +47,6 @@ import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; -import static bisq.core.api.model.OfferInfo.toOfferInfo; import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import static bisq.proto.grpc.OffersGrpc.*; import static java.util.concurrent.TimeUnit.SECONDS; @@ -75,7 +74,7 @@ class GrpcOffersService extends OffersImplBase { try { Offer offer = coreApi.getOffer(req.getId()); var reply = GetOfferReply.newBuilder() - .setOffer(toOfferInfo(offer).toProtoMessage()) + .setOffer(OfferInfo.toOfferInfo(offer).toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -84,14 +83,14 @@ class GrpcOffersService extends OffersImplBase { } } + // TODO: merge with getOffer()? @Override public void getMyOffer(GetMyOfferRequest req, StreamObserver responseObserver) { try { - Offer offer = coreApi.getMyOffer(req.getId()); OpenOffer openOffer = coreApi.getMyOpenOffer(req.getId()); var reply = GetMyOfferReply.newBuilder() - .setOffer(toOfferInfo(offer, openOffer).toProtoMessage()) + .setOffer(OfferInfo.toMyOfferInfo(openOffer).toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -126,7 +125,7 @@ class GrpcOffersService extends OffersImplBase { List result = new ArrayList(); for (Offer offer : coreApi.getMyOffers(req.getDirection(), req.getCurrencyCode())) { OpenOffer openOffer = coreApi.getMyOpenOffer(offer.getId()); - result.add(toOfferInfo(offer, openOffer)); + result.add(OfferInfo.toMyOfferInfo(openOffer)); } var reply = GetMyOffersReply.newBuilder() .addAllOffers(result.stream() @@ -154,17 +153,17 @@ class GrpcOffersService extends OffersImplBase { req.getDirection(), req.getPrice(), req.getUseMarketBasedPrice(), - req.getMarketPriceMargin(), + req.getMarketPriceMarginPct(), ParsingUtils.atomicUnitsToCentineros(req.getAmount()), // scale atomic unit to centineros for consistency TODO switch base to atomic units? ParsingUtils.atomicUnitsToCentineros(req.getMinAmount()), - req.getBuyerSecurityDeposit(), + req.getBuyerSecurityDepositPct(), req.getTriggerPrice(), req.getPaymentAccountId(), offer -> { // This result handling consumer's accept operation will return // the new offer to the gRPC client after async placement is done. OpenOffer openOffer = coreApi.getMyOpenOffer(offer.getId()); - OfferInfo offerInfo = toOfferInfo(offer, openOffer); + OfferInfo offerInfo = OfferInfo.toMyOfferInfo(openOffer); CreateOfferReply reply = CreateOfferReply.newBuilder() .setOffer(offerInfo.toProtoMessage()) .build(); diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css index dc97dfc84b..125656c5a2 100644 --- a/desktop/src/main/java/bisq/desktop/bisq.css +++ b/desktop/src/main/java/bisq/desktop/bisq.css @@ -175,6 +175,14 @@ -fx-padding: 0 10 0 10; } +.tiny-button, +.action-button.tiny-button { + -fx-font-size: 0.769em; + -fx-pref-height: 20; + -fx-padding: 3 8 3 8; + -fx-border-radius: 5; +} + .text-button { -fx-background-color: transparent; -fx-underline: true; @@ -601,6 +609,10 @@ tree-table-view:focused { -fx-font-size: 0.769em; } +.medium-text { + -fx-font-size: 0.846em; +} + .normal-text { -fx-font-size: 0.923em; } @@ -609,6 +621,11 @@ tree-table-view:focused { -fx-font-size: 13; } +.bold-text, +.bold-text .text { + -fx-font-weight: bold; +} + /* Splash */ #splash { -fx-background-color: -bs-background-color; @@ -648,7 +665,7 @@ tree-table-view:focused { } .top-navigation .separator:vertical .line { - -fx-border-color: transparent transparent transparent -bs-rd-nav-border-color; + -fx-border-color: transparent transparent transparent transparent; -fx-border-width: 1; -fx-border-insets: 0 0 0 1; } @@ -839,6 +856,14 @@ tree-table-view:focused { -fx-fill: -bs-text-color; } +.link-icon { + -fx-fill: -bs-color-gray-ccc; +} + +.link-icon:hover { + -fx-fill: -fx-accent; +} + /******************************************************************************* * * * Tooltip * @@ -1740,36 +1765,119 @@ textfield */ #charts-dao .default-color1.chart-line-symbol { -fx-background-color: -bs-chart-dao-line2, -bs-chart-dao-line2; } #charts-dao .default-color2.chart-series-line { -fx-stroke: -bs-chart-dao-line3; } -#charts-dao .default-color2.chart-line-symbol { -fx-background-color: -bs-chart-dao-line3, -bs-chart-dao-line3; } +#charts-dao .default-color2.chart-line-symbol { + -fx-background-color: -bs-chart-dao-line3, -bs-chart-dao-line3; +} -#charts-dao .default-color3.chart-series-line { -fx-stroke: -bs-chart-dao-line4; } -#charts-dao .default-color3.chart-line-symbol { -fx-background-color: -bs-chart-dao-line4, -bs-chart-dao-line4; } +#charts-dao .default-color3.chart-series-line { + -fx-stroke: -bs-chart-dao-line4; +} -#charts-dao .default-color4.chart-series-line { -fx-stroke: -bs-chart-dao-line5; } -#charts-dao .default-color4.chart-line-symbol { -fx-background-color: -bs-chart-dao-line5, -bs-chart-dao-line5; } +#charts-dao .default-color3.chart-line-symbol { + -fx-background-color: -bs-chart-dao-line4, -bs-chart-dao-line4; +} -#charts-dao .default-color5.chart-series-line { -fx-stroke: -bs-chart-dao-line6; } -#charts-dao .default-color5.chart-line-symbol { -fx-background-color: -bs-chart-dao-line6, -bs-chart-dao-line6; } +#charts-dao .default-color4.chart-series-line { + -fx-stroke: -bs-chart-dao-line5; +} + +#charts-dao .default-color4.chart-line-symbol { + -fx-background-color: -bs-chart-dao-line5, -bs-chart-dao-line5; +} + +#charts-dao .default-color5.chart-series-line { + -fx-stroke: -bs-chart-dao-line6; +} + +#charts-dao .default-color5.chart-line-symbol { + -fx-background-color: -bs-chart-dao-line6, -bs-chart-dao-line6; +} + +#charts-dao .default-color6.chart-series-line { + -fx-stroke: -bs-chart-dao-line7; +} + +#charts-dao .default-color6.chart-line-symbol { + -fx-background-color: -bs-chart-dao-line7, -bs-chart-dao-line7; +} + +#charts-dao .default-color7.chart-series-line { + -fx-stroke: -bs-chart-dao-line8; +} + +#charts-dao .default-color7.chart-line-symbol { + -fx-background-color: -bs-chart-dao-line8, -bs-chart-dao-line8; +} + +#charts-dao .default-color8.chart-series-line { + -fx-stroke: -bs-chart-dao-line9; +} + +#charts-dao .default-color8.chart-line-symbol { + -fx-background-color: -bs-chart-dao-line9, -bs-chart-dao-line9; +} + +#charts-dao .default-color9.chart-series-line { + -fx-stroke: -bs-chart-dao-line10; +} + +#charts-dao .default-color9.chart-line-symbol { + -fx-background-color: -bs-chart-dao-line10, -bs-chart-dao-line10; +} + + +#charts-dao .default-color10.chart-series-line { + -fx-stroke: -bs-chart-dao-line11; +} + +#charts-dao .default-color10.chart-line-symbol { + -fx-background-color: -bs-chart-dao-line11, -bs-chart-dao-line11; +} #charts-legend-toggle0 { -jfx-toggle-color: -bs-chart-dao-line1 } + #charts-legend-toggle1 { -jfx-toggle-color: -bs-chart-dao-line2; } + #charts-legend-toggle2 { -jfx-toggle-color: -bs-chart-dao-line3; } + #charts-legend-toggle3 { -jfx-toggle-color: -bs-chart-dao-line4; } + #charts-legend-toggle4 { -jfx-toggle-color: -bs-chart-dao-line5; } + #charts-legend-toggle5 { -jfx-toggle-color: -bs-chart-dao-line6; } +#charts-legend-toggle6 { + -jfx-toggle-color: -bs-chart-dao-line7; +} + +#charts-legend-toggle7 { + -jfx-toggle-color: -bs-chart-dao-line8; +} + +#charts-legend-toggle8 { + -jfx-toggle-color: -bs-chart-dao-line9; +} + +#charts-legend-toggle9 { + -jfx-toggle-color: -bs-chart-dao-line10; +} + +#charts-legend-toggle10 { + -jfx-toggle-color: -bs-chart-dao-line11; +} + #charts-dao .chart-series-line { -fx-stroke-width: 2px; } @@ -1916,6 +2024,21 @@ textfield */ -fx-effect: dropshadow(gaussian, -bs-text-color-transparent-dark, 44, 0, -1, 3); } +.account-status-title { + -fx-font-size: 0.769em; + -fx-font-family: "IBM Plex Sans Medium"; +} + +.account-status-inactive-info-item { + -fx-text-fill: -bs-color-gray-dim; + -fx-fill: -bs-color-gray-dim; +} + +.account-status-active-info-item { + -fx-text-fill: -fx-accent; + -fx-fill: -fx-accent; +} + #price-feed-combo { -fx-background-color: none; } @@ -2004,7 +2127,7 @@ textfield */ } .dispute-chat-border { - -fx-background-color: -bs-color-blue-5; + -fx-background-color: -bs-support-chat-background; } /******************************************************************************************************************** @@ -2077,6 +2200,11 @@ textfield */ -fx-text-fill: -bs-color-green-3; } +.dao-tx-type-bsq-swap-icon, +.dao-tx-type-bsq-swap-icon:hover { + -fx-text-fill: -bs-color-blue-4; +} + .dao-accepted-icon { -fx-text-fill: -bs-color-primary; } @@ -2129,7 +2257,7 @@ textfield */ -fx-text-fill: -bs-rd-font-dark; } -.dao-news-content, .dao-news-section-link, .dao-news-section-link .hyperlink, .dao-launch-version { +.dao-news-content, .dao-news-section-link, .dao-news-section-link .hyperlink { -fx-text-fill: -bs-rd-font-light; -fx-fill: -bs-rd-font-light; } diff --git a/desktop/src/main/java/bisq/desktop/components/AccountStatusTooltipLabel.java b/desktop/src/main/java/bisq/desktop/components/AccountStatusTooltipLabel.java new file mode 100644 index 0000000000..edc5382034 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/AccountStatusTooltipLabel.java @@ -0,0 +1,164 @@ +/* + * 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 . + */ + +package bisq.desktop.components; + +import bisq.desktop.components.controlsfx.control.PopOver; +import bisq.desktop.main.offer.offerbook.OfferBookListItem; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; + +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.locale.Res; +import bisq.core.offer.OfferRestrictions; +import bisq.core.util.coin.CoinFormatter; + +import bisq.common.UserThread; + +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; + +import javafx.scene.Node; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +import javafx.geometry.Bounds; +import javafx.geometry.Insets; +import javafx.geometry.Pos; + +import java.util.concurrent.TimeUnit; + + +public class AccountStatusTooltipLabel extends AutoTooltipLabel { + + public static final int DEFAULT_WIDTH = 300; + private final Node textIcon; + private final OfferBookListItem.WitnessAgeData witnessAgeData; + private final String popupTitle; + private PopOver popOver; + private boolean keepPopOverVisible = false; + + public AccountStatusTooltipLabel(OfferBookListItem.WitnessAgeData witnessAgeData, + CoinFormatter formatter) { + super(witnessAgeData.getDisplayString()); + this.witnessAgeData = witnessAgeData; + this.textIcon = FormBuilder.getIcon(witnessAgeData.getIcon()); + this.popupTitle = witnessAgeData.isLimitLifted() + ? Res.get("offerbook.timeSinceSigning.tooltip.accountLimitLifted") + : Res.get("offerbook.timeSinceSigning.tooltip.accountLimit", formatter.formatCoinWithCode(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT)); + + positionAndActivateIcon(); + } + + private void positionAndActivateIcon() { + textIcon.setOpacity(0.4); + textIcon.getStyleClass().add("tooltip-icon"); + popOver = createPopOver(); + textIcon.setOnMouseEntered(e -> showPopup(textIcon)); + + textIcon.setOnMouseExited(e -> UserThread.runAfter(() -> { + if (!keepPopOverVisible) { + popOver.hide(); + } + }, 200, TimeUnit.MILLISECONDS) + ); + + setGraphic(textIcon); + setContentDisplay(ContentDisplay.RIGHT); + } + + private PopOver createPopOver() { + Label titleLabel = new Label(popupTitle); + titleLabel.setMaxWidth(DEFAULT_WIDTH); + titleLabel.setWrapText(true); + titleLabel.setPadding(new Insets(10, 10, 0, 10)); + titleLabel.getStyleClass().add("account-status-title"); + + Label infoLabel = new Label(witnessAgeData.getInfo()); + infoLabel.setMaxWidth(DEFAULT_WIDTH); + infoLabel.setWrapText(true); + infoLabel.setPadding(new Insets(0, 10, 4, 10)); + infoLabel.getStyleClass().add("small-text"); + + Label buyLabel = createDetailsItem( + Res.get("offerbook.timeSinceSigning.tooltip.checkmark.buyBtc"), + witnessAgeData.isAccountSigned() + ); + Label waitLabel = createDetailsItem( + Res.get("offerbook.timeSinceSigning.tooltip.checkmark.wait", SignedWitnessService.SIGNER_AGE_DAYS), + witnessAgeData.isLimitLifted() + ); + + Hyperlink learnMoreLink = new ExternalHyperlink(Res.get("offerbook.timeSinceSigning.tooltip.learnMore"), + null, + "0.769em"); + learnMoreLink.setMaxWidth(DEFAULT_WIDTH); + learnMoreLink.setWrapText(true); + learnMoreLink.setPadding(new Insets(10, 10, 2, 10)); + learnMoreLink.getStyleClass().addAll("very-small-text"); + learnMoreLink.setOnAction((e) -> GUIUtil.openWebPage("https://bisq.wiki/Account_limits")); + + VBox vBox = new VBox(2, titleLabel, infoLabel, buyLabel, waitLabel, learnMoreLink); + vBox.setPadding(new Insets(2, 0, 2, 0)); + vBox.setAlignment(Pos.CENTER_LEFT); + + + PopOver popOver = new PopOver(vBox); + popOver.setArrowLocation(PopOver.ArrowLocation.LEFT_CENTER); + + vBox.setOnMouseEntered(mouseEvent -> keepPopOverVisible = true); + + vBox.setOnMouseExited(mouseEvent -> { + keepPopOverVisible = false; + popOver.hide(); + }); + + return popOver; + } + + private void showPopup(Node textIcon) { + Bounds bounds = textIcon.localToScreen(textIcon.getBoundsInLocal()); + popOver.show(textIcon, bounds.getMaxX() + 10, (bounds.getMinY() + bounds.getHeight() / 2) - 10); + } + + private Label createDetailsItem(String text, boolean active) { + Label label = new Label(text); + label.setMaxWidth(DEFAULT_WIDTH); + label.setWrapText(true); + label.setPadding(new Insets(0, 10, 0, 10)); + label.getStyleClass().add("small-text"); + if (active) { + label.setStyle("-fx-text-fill: -fx-accent"); + } else { + label.setStyle("-fx-text-fill: -bs-color-gray-dim"); + } + + Text icon = FormBuilder.getSmallIconForLabel(active ? + MaterialDesignIcon.CHECKBOX_MARKED_CIRCLE : MaterialDesignIcon.CLOSE_CIRCLE, label); + icon.setLayoutY(4); + + if (active) { + icon.getStyleClass().add("account-status-active-info-item"); + } else { + icon.getStyleClass().add("account-status-inactive-info-item"); + } + + return label; + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java b/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java index 90bcae5cdb..68f6af102c 100644 --- a/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java +++ b/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java @@ -18,8 +18,8 @@ package bisq.desktop.components; import bisq.desktop.components.controlsfx.control.PopOver; +import bisq.desktop.util.FormBuilder; -import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import javafx.scene.Node; @@ -48,8 +48,7 @@ public class AutoTooltipTableColumn extends TableColumn { } public void setTitleWithHelpText(String title, String help) { - helpIcon = new Label(); - AwesomeDude.setIcon(helpIcon, AwesomeIcon.QUESTION_SIGN, "1em"); + helpIcon = FormBuilder.getSmallIcon(AwesomeIcon.QUESTION_SIGN); helpIcon.setOpacity(0.4); helpIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createInfoPopOver(help))); helpIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver()); diff --git a/desktop/src/main/java/bisq/desktop/components/AutocompleteComboBox.java b/desktop/src/main/java/bisq/desktop/components/AutocompleteComboBox.java index 811d96ef59..fcadf8f02c 100644 --- a/desktop/src/main/java/bisq/desktop/components/AutocompleteComboBox.java +++ b/desktop/src/main/java/bisq/desktop/components/AutocompleteComboBox.java @@ -35,6 +35,7 @@ import javafx.collections.ObservableList; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; /** * Implements searchable dropdown (an autocomplete like experience). @@ -44,8 +45,9 @@ import java.util.List; * @param type of the ComboBox item; in the simplest case this can be a String */ public class AutocompleteComboBox extends JFXComboBox { - private ArrayList completeList; - private ArrayList matchingList; + private List list; + private List extendedList; + private List matchingList; private JFXComboBoxListViewSkin comboBoxListViewSkin; public AutocompleteComboBox() { @@ -65,15 +67,20 @@ public class AutocompleteComboBox extends JFXComboBox { /** * Set the complete list of ComboBox items. Use this instead of setItems(). */ - public void setAutocompleteItems(List items) { - completeList = new ArrayList<>(items); - matchingList = new ArrayList<>(completeList); + public void setAutocompleteItems(List items, List allItems) { + list = items; + extendedList = allItems; + matchingList = new ArrayList<>(list); setValue(null); getSelectionModel().clearSelection(); setItems(FXCollections.observableList(matchingList)); getEditor().setText(""); } + public void setAutocompleteItems(List items) { + setAutocompleteItems(items, null); + } + /** * Triggered when value change is *confirmed*. In practical terms * this is when user clicks item on the dropdown or hits [ENTER] @@ -135,11 +142,11 @@ public class AutocompleteComboBox extends JFXComboBox { } private void filterBy(String query) { - ArrayList newMatchingList = new ArrayList<>(); - for (T item : completeList) - if (StringUtils.containsIgnoreCase(asString(item), query)) - newMatchingList.add(item); - matchingList = newMatchingList; + matchingList = (extendedList != null && query.length() > 0 ? extendedList : list) + .stream() + .filter(item -> StringUtils.containsIgnoreCase(asString(item), query)) + .collect(Collectors.toList()); + setValue(null); getSelectionModel().clearSelection(); setItems(FXCollections.observableList(matchingList)); @@ -153,7 +160,7 @@ public class AutocompleteComboBox extends JFXComboBox { getEditor().addEventHandler(KeyEvent.KEY_RELEASED, (KeyEvent event) -> { UserThread.execute(() -> { String query = getEditor().getText(); - var exactMatch = completeList.stream().anyMatch(item -> asString(item).equalsIgnoreCase(query)); + var exactMatch = list.stream().anyMatch(item -> asString(item).equalsIgnoreCase(query)); if (!exactMatch) { if (query.isEmpty()) removeFilter(); @@ -166,7 +173,7 @@ public class AutocompleteComboBox extends JFXComboBox { } private void removeFilter() { - matchingList = new ArrayList<>(completeList); + matchingList = new ArrayList<>(list); setValue(null); getSelectionModel().clearSelection(); setItems(FXCollections.observableList(matchingList)); diff --git a/desktop/src/main/java/bisq/desktop/components/ExplorerAddressTextField.java b/desktop/src/main/java/bisq/desktop/components/ExplorerAddressTextField.java new file mode 100644 index 0000000000..54a48fd2e1 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/ExplorerAddressTextField.java @@ -0,0 +1,129 @@ +/* + * 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 . + */ + +package bisq.desktop.components; + +import bisq.desktop.util.GUIUtil; + +import bisq.core.locale.Res; +import bisq.core.user.BlockChainExplorer; +import bisq.core.user.Preferences; + +import bisq.common.util.Utilities; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; + +import com.jfoenix.controls.JFXTextField; + +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.AnchorPane; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; + +public class ExplorerAddressTextField extends AnchorPane { + @Setter + private static Preferences preferences; + + @Getter + private final TextField textField; + private final Label copyIcon, blockExplorerIcon, missingAddressWarningIcon; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public ExplorerAddressTextField() { + copyIcon = new Label(); + copyIcon.setLayoutY(3); + copyIcon.getStyleClass().addAll("icon", "highlight"); + copyIcon.setTooltip(new Tooltip(Res.get("explorerAddressTextField.copyToClipboard"))); + AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); + AnchorPane.setRightAnchor(copyIcon, 30.0); + + Tooltip tooltip = new Tooltip(Res.get("explorerAddressTextField.blockExplorerIcon.tooltip")); + + blockExplorerIcon = new Label(); + blockExplorerIcon.getStyleClass().addAll("icon", "highlight"); + blockExplorerIcon.setTooltip(tooltip); + AwesomeDude.setIcon(blockExplorerIcon, AwesomeIcon.EXTERNAL_LINK); + blockExplorerIcon.setMinWidth(20); + AnchorPane.setRightAnchor(blockExplorerIcon, 52.0); + AnchorPane.setTopAnchor(blockExplorerIcon, 4.0); + + missingAddressWarningIcon = new Label(); + missingAddressWarningIcon.getStyleClass().addAll("icon", "error-icon"); + AwesomeDude.setIcon(missingAddressWarningIcon, AwesomeIcon.WARNING_SIGN); + missingAddressWarningIcon.setTooltip(new Tooltip(Res.get("explorerAddressTextField.missingTx.warning.tooltip"))); + missingAddressWarningIcon.setMinWidth(20); + AnchorPane.setRightAnchor(missingAddressWarningIcon, 52.0); + AnchorPane.setTopAnchor(missingAddressWarningIcon, 4.0); + missingAddressWarningIcon.setVisible(false); + missingAddressWarningIcon.setManaged(false); + + textField = new JFXTextField(); + textField.setId("address-text-field"); + textField.setEditable(false); + textField.setTooltip(tooltip); + AnchorPane.setRightAnchor(textField, 80.0); + AnchorPane.setLeftAnchor(textField, 0.0); + textField.focusTraversableProperty().set(focusTraversableProperty().get()); + getChildren().addAll(textField, missingAddressWarningIcon, blockExplorerIcon, copyIcon); + } + + public void setup(@Nullable String address) { + if (address == null) { + textField.setText(Res.get("shared.na")); + textField.setId("address-text-field-error"); + blockExplorerIcon.setVisible(false); + blockExplorerIcon.setManaged(false); + copyIcon.setVisible(false); + copyIcon.setManaged(false); + missingAddressWarningIcon.setVisible(true); + missingAddressWarningIcon.setManaged(true); + return; + } + + textField.setText(address); + textField.setOnMouseClicked(mouseEvent -> openBlockExplorer(address)); + blockExplorerIcon.setOnMouseClicked(mouseEvent -> openBlockExplorer(address)); + copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(address)); + } + + public void cleanup() { + textField.setOnMouseClicked(null); + blockExplorerIcon.setOnMouseClicked(null); + copyIcon.setOnMouseClicked(null); + textField.setText(""); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void openBlockExplorer(String address) { + if (preferences != null) { + BlockChainExplorer blockChainExplorer = preferences.getBlockChainExplorer(); + GUIUtil.openWebPage(blockChainExplorer.addressUrl + address, false); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/ExternalHyperlink.java b/desktop/src/main/java/bisq/desktop/components/ExternalHyperlink.java index 61323e8f95..091c4433ca 100644 --- a/desktop/src/main/java/bisq/desktop/components/ExternalHyperlink.java +++ b/desktop/src/main/java/bisq/desktop/components/ExternalHyperlink.java @@ -28,4 +28,8 @@ public class ExternalHyperlink extends HyperlinkWithIcon { public ExternalHyperlink(String text, String style) { super(text, MaterialDesignIcon.LINK, style); } + + public ExternalHyperlink(String text, String style, String iconSize) { + super(text, MaterialDesignIcon.LINK, style, iconSize); + } } diff --git a/desktop/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java b/desktop/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java index a10073ad8d..5ec6711a04 100644 --- a/desktop/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java +++ b/desktop/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java @@ -41,11 +41,15 @@ public class HyperlinkWithIcon extends Hyperlink { this(text, AwesomeIcon.INFO_SIGN); } - public HyperlinkWithIcon(String text, AwesomeIcon awesomeIcon) { + public HyperlinkWithIcon(String text, String fontSize) { + this(text, AwesomeIcon.INFO_SIGN, fontSize); + } + + public HyperlinkWithIcon(String text, AwesomeIcon awesomeIcon, String fontSize) { super(text); Label icon = new Label(); - AwesomeDude.setIcon(icon, awesomeIcon); + AwesomeDude.setIcon(icon, awesomeIcon, fontSize); icon.setMinWidth(20); icon.setOpacity(0.7); icon.getStyleClass().addAll("hyperlink", "no-underline"); @@ -55,14 +59,18 @@ public class HyperlinkWithIcon extends Hyperlink { setIcon(icon); } + public HyperlinkWithIcon(String text, AwesomeIcon awesomeIcon) { + this(text, awesomeIcon, "1.231em"); + } + public HyperlinkWithIcon(String text, GlyphIcons icon) { this(text, icon, null); } - public HyperlinkWithIcon(String text, GlyphIcons icon, String style) { + public HyperlinkWithIcon(String text, GlyphIcons icon, String style, String iconSize) { super(text); - Text textIcon = FormBuilder.getIcon(icon); + Text textIcon = FormBuilder.getIcon(icon, iconSize); textIcon.setOpacity(0.7); textIcon.getStyleClass().addAll("hyperlink", "no-underline"); @@ -76,6 +84,10 @@ public class HyperlinkWithIcon extends Hyperlink { setIcon(textIcon); } + public HyperlinkWithIcon(String text, GlyphIcons icon, String style) { + this(text, icon, style, "1.231em"); + } + public void hideIcon() { setGraphic(null); } diff --git a/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java b/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java index ce4495abb4..947abb8635 100644 --- a/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java +++ b/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java @@ -74,6 +74,12 @@ public class InfoAutoTooltipLabel extends AutoTooltipLabel { setGraphic(textIcon); } + // May be required until https://bugs.openjdk.java.net/browse/JDK-8265835 is fixed. + public void disableRolloverPopup() { + textIcon.setOnMouseEntered(null); + textIcon.setOnMouseExited(null); + } + private void positionAndActivateIcon(ContentDisplay contentDisplay, String info, double width) { textIcon.setOpacity(0.4); textIcon.getStyleClass().add("tooltip-icon"); diff --git a/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java b/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java index 7054d079f6..5d4f18ca5c 100644 --- a/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java +++ b/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java @@ -98,6 +98,7 @@ public class InfoInputTextField extends AnchorPane { public void setContentForPopOver(Node node, AwesomeIcon awesomeIcon, @Nullable String style) { this.node = node; AwesomeDude.setIcon(icon, awesomeIcon); + icon.getStyleClass().removeAll("icon", "info", "warning", style); icon.getStyleClass().addAll("icon", style == null ? "info" : style); icon.setManaged(true); icon.setVisible(true); diff --git a/desktop/src/main/java/bisq/desktop/components/InfoTextField.java b/desktop/src/main/java/bisq/desktop/components/InfoTextField.java index 54f6f64cbc..46d55fc5b5 100644 --- a/desktop/src/main/java/bisq/desktop/components/InfoTextField.java +++ b/desktop/src/main/java/bisq/desktop/components/InfoTextField.java @@ -19,8 +19,6 @@ package bisq.desktop.components; import bisq.desktop.components.controlsfx.control.PopOver; -import bisq.common.UserThread; - import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; diff --git a/desktop/src/main/java/bisq/desktop/components/InputTextField.java b/desktop/src/main/java/bisq/desktop/components/InputTextField.java index 2fdfddd652..edd9c93b84 100644 --- a/desktop/src/main/java/bisq/desktop/components/InputTextField.java +++ b/desktop/src/main/java/bisq/desktop/components/InputTextField.java @@ -20,6 +20,7 @@ package bisq.desktop.components; import bisq.desktop.util.validation.JFXInputValidator; +import bisq.core.locale.Res; import bisq.core.util.validation.InputValidator; import com.jfoenix.controls.JFXTextField; @@ -133,6 +134,10 @@ public class InputTextField extends JFXTextField { } } + public void setInvalid(String message) { + validationResult.set(new InputValidator.ValidationResult(false, message)); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java b/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java index 1761607786..8b7ca3d505 100644 --- a/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java +++ b/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java @@ -20,23 +20,16 @@ package bisq.desktop.components; import bisq.desktop.main.overlays.editor.PeerInfoWithTagEditor; import bisq.desktop.util.DisplayUtils; -import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; -import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.offer.Offer; -import bisq.core.payment.payload.PaymentMethod; import bisq.core.trade.Trade; import bisq.core.user.Preferences; import bisq.network.p2p.NodeAddress; -import bisq.common.util.Tuple5; - import com.google.common.base.Charsets; -import org.apache.commons.lang3.StringUtils; - import javafx.scene.Group; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; @@ -51,132 +44,37 @@ import javafx.geometry.Point2D; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Date; import java.util.Map; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; -import static com.google.common.base.Preconditions.checkNotNull; - @Slf4j public class PeerInfoIcon extends Group { - private final String tooltipText; - private final int numTrades; - private final AccountAgeWitnessService accountAgeWitnessService; - private final Map peerTagMap; - private final Label numTradesLabel; - private final Label tagLabel; - final Pane tagPane; - final Pane numTradesPane; - private final String fullAddress; - - public PeerInfoIcon(NodeAddress nodeAddress, - String role, - int numTrades, - PrivateNotificationManager privateNotificationManager, - Offer offer, - Preferences preferences, - AccountAgeWitnessService accountAgeWitnessService, - boolean useDevPrivilegeKeys) { - this(nodeAddress, - role, - numTrades, - privateNotificationManager, - offer, - null, - preferences, - accountAgeWitnessService, - useDevPrivilegeKeys); - + public interface notify { + void avatarTagUpdated(); } - public PeerInfoIcon(NodeAddress nodeAddress, - String role, - int numTrades, - PrivateNotificationManager privateNotificationManager, - Trade trade, - Preferences preferences, - AccountAgeWitnessService accountAgeWitnessService, - boolean useDevPrivilegeKeys) { - this(nodeAddress, - role, - numTrades, - privateNotificationManager, - trade.getOffer(), - trade, - preferences, - accountAgeWitnessService, - useDevPrivilegeKeys); + @Setter + private notify callback; + protected Preferences preferences; + protected final String fullAddress; + protected String tooltipText; + protected Label tagLabel; + private Label numTradesLabel; + protected Pane tagPane; + protected Pane numTradesPane; + protected int numTrades = 0; + + public PeerInfoIcon(NodeAddress nodeAddress, Preferences preferences) { + this.preferences = preferences; + this.fullAddress = nodeAddress != null ? nodeAddress.getFullAddress() : ""; } - private PeerInfoIcon(NodeAddress nodeAddress, - String role, - int numTrades, - PrivateNotificationManager privateNotificationManager, - @Nullable Offer offer, - @Nullable Trade trade, - Preferences preferences, - AccountAgeWitnessService accountAgeWitnessService, - boolean useDevPrivilegeKeys) { - this.numTrades = numTrades; - this.accountAgeWitnessService = accountAgeWitnessService; - + protected void createAvatar(Color ringColor) { double scaleFactor = getScaleFactor(); - fullAddress = nodeAddress != null ? nodeAddress.getFullAddress() : ""; - - peerTagMap = preferences.getPeerTagMap(); - - boolean hasTraded = numTrades > 0; - Tuple5 peersAccount = getPeersAccountAge(trade, offer); - - Long accountAge = peersAccount.first; - Long signAge = peersAccount.second; - - if (offer == null) { - checkNotNull(trade, "Trade must not be null if offer is null."); - offer = trade.getOffer(); - } - - checkNotNull(offer, "Offer must not be null"); - - boolean isFiatCurrency = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()); - - String accountAgeTooltip = isFiatCurrency ? - accountAge > -1 ? Res.get("peerInfoIcon.tooltip.age", DisplayUtils.formatAccountAge(accountAge)) : - Res.get("peerInfoIcon.tooltip.unknownAge") : - ""; - tooltipText = hasTraded ? - Res.get("peerInfoIcon.tooltip.trade.traded", role, fullAddress, numTrades, accountAgeTooltip) : - Res.get("peerInfoIcon.tooltip.trade.notTraded", role, fullAddress, accountAgeTooltip); - - // outer circle - Color ringColor; - if (isFiatCurrency) { - - switch (accountAgeWitnessService.getPeersAccountAgeCategory(hasChargebackRisk(trade, offer) ? signAge : accountAge)) { - case TWO_MONTHS_OR_MORE: - ringColor = Color.rgb(0, 225, 0); // > 2 months green - break; - case ONE_TO_TWO_MONTHS: - ringColor = Color.rgb(0, 139, 205); // 1-2 months blue - break; - case LESS_ONE_MONTH: - ringColor = Color.rgb(255, 140, 0); //< 1 month orange - break; - case UNVERIFIED: - default: - ringColor = Color.rgb(255, 0, 0); // not signed, red - break; - } - - - } else { - // for altcoins we always display green - ringColor = Color.rgb(0, 225, 0); - } - double outerSize = 26 * scaleFactor; Canvas outerBackground = new Canvas(outerSize, outerSize); GraphicsContext outerBackgroundGc = outerBackground.getGraphicsContext2D(); @@ -245,54 +143,6 @@ public class PeerInfoIcon extends Group { updatePeerInfoIcon(); getChildren().addAll(outerBackground, innerBackground, avatarImageView, tagPane, numTradesPane); - - addMouseListener(numTrades, privateNotificationManager, trade, offer, preferences, useDevPrivilegeKeys, - isFiatCurrency, accountAge, signAge, peersAccount.third, peersAccount.fourth, peersAccount.fifth); - } - - /** - * @param trade Open trade for trading peer info to be shown - * @param offer Open offer for trading peer info to be shown - * @return account age, sign age, account info, sign info, sign state - */ - private Tuple5 getPeersAccountAge(@Nullable Trade trade, - @Nullable Offer offer) { - AccountAgeWitnessService.SignState signState; - long signAge = -1L; - long accountAge = -1L; - - if (trade != null) { - offer = trade.getOffer(); - if (offer == null) { - // unexpected - return new Tuple5<>(signAge, accountAge, Res.get("peerInfo.age.noRisk"), null, null); - } - signState = accountAgeWitnessService.getSignState(trade); - signAge = accountAgeWitnessService.getWitnessSignAge(trade, new Date()); - accountAge = accountAgeWitnessService.getAccountAge(trade); - } else { - checkNotNull(offer, "Offer must not be null if trade is null."); - signState = accountAgeWitnessService.getSignState(offer); - signAge = accountAgeWitnessService.getWitnessSignAge(offer, new Date()); - accountAge = accountAgeWitnessService.getAccountAge(offer); - } - - if (hasChargebackRisk(trade, offer)) { - String signAgeInfo = Res.get("peerInfo.age.chargeBackRisk"); - String accountSigningState = StringUtils.capitalize(signState.getDisplayString()); - if (signState.equals(AccountAgeWitnessService.SignState.UNSIGNED)) - signAgeInfo = null; - - return new Tuple5<>(accountAge, signAge, Res.get("peerInfo.age.noRisk"), signAgeInfo, accountSigningState); - } - return new Tuple5<>(accountAge, signAge, Res.get("peerInfo.age.noRisk"), null, null); - } - - private boolean hasChargebackRisk(@Nullable Trade trade, @Nullable Offer offer) { - Offer offerToCheck = trade != null ? trade.getOffer() : offer; - - return offerToCheck != null && - PaymentMethod.hasChargebackRisk(offerToCheck.getPaymentMethod(), offerToCheck.getCurrencyCode()); } protected void addMouseListener(int numTrades, @@ -332,6 +182,9 @@ public class PeerInfoIcon extends Group { .onSave(newTag -> { preferences.setTagForPeer(fullAddress, newTag); updatePeerInfoIcon(); + if (callback != null) { + callback.avatarTagUpdated(); + } }) .show()); } @@ -340,8 +193,15 @@ public class PeerInfoIcon extends Group { return 1; } + protected String getAccountAgeTooltip(Long accountAge) { + return accountAge > -1 ? + Res.get("peerInfoIcon.tooltip.age", DisplayUtils.formatAccountAge(accountAge)) : + Res.get("peerInfoIcon.tooltip.unknownAge"); + } + protected void updatePeerInfoIcon() { String tag; + Map peerTagMap = preferences.getPeerTagMap(); if (peerTagMap.containsKey(fullAddress)) { tag = peerTagMap.get(fullAddress); final String text = !tag.isEmpty() ? Res.get("peerInfoIcon.tooltip", tooltipText, tag) : tooltipText; diff --git a/desktop/src/main/java/bisq/desktop/components/PeerInfoIconDispute.java b/desktop/src/main/java/bisq/desktop/components/PeerInfoIconDispute.java new file mode 100644 index 0000000000..539d0a3a37 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/PeerInfoIconDispute.java @@ -0,0 +1,49 @@ +/* + * 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 . + */ + +package bisq.desktop.components; + +import bisq.core.locale.Res; +import bisq.core.user.Preferences; + +import bisq.network.p2p.NodeAddress; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.Colors.AVATAR_GREY; + +@Slf4j +public class PeerInfoIconDispute extends PeerInfoIcon { + + public PeerInfoIconDispute(NodeAddress nodeAddress, + String nrOfDisputes, + long accountAge, + Preferences preferences) { + super(nodeAddress, preferences); + + tooltipText = Res.get("peerInfoIcon.tooltip.dispute", fullAddress, nrOfDisputes, getAccountAgeTooltip(accountAge)); + + // outer circle always display gray + createAvatar(AVATAR_GREY); + addMouseListener(numTrades, null, null, null, preferences, false, + false, accountAge, 0L, null, null, null); + } + + public void refreshTag() { + updatePeerInfoIcon(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/PeerInfoIconSmall.java b/desktop/src/main/java/bisq/desktop/components/PeerInfoIconSmall.java index cee0bec7fd..5d444f3bd4 100644 --- a/desktop/src/main/java/bisq/desktop/components/PeerInfoIconSmall.java +++ b/desktop/src/main/java/bisq/desktop/components/PeerInfoIconSmall.java @@ -10,7 +10,7 @@ import bisq.network.p2p.NodeAddress; import javax.annotation.Nullable; -public class PeerInfoIconSmall extends PeerInfoIcon { +public class PeerInfoIconSmall extends PeerInfoIconTrading { public PeerInfoIconSmall(NodeAddress nodeAddress, String role, Offer offer, @@ -36,7 +36,7 @@ public class PeerInfoIconSmall extends PeerInfoIcon { @Override protected void addMouseListener(int numTrades, PrivateNotificationManager privateNotificationManager, - @Nullable Trade trade, + @Nullable Trade tradeModel, Offer offer, Preferences preferences, boolean useDevPrivilegeKeys, diff --git a/desktop/src/main/java/bisq/desktop/components/PeerInfoIconTrading.java b/desktop/src/main/java/bisq/desktop/components/PeerInfoIconTrading.java new file mode 100644 index 0000000000..06c257c2c8 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/PeerInfoIconTrading.java @@ -0,0 +1,209 @@ +/* + * 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 . + */ + +package bisq.desktop.components; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.locale.Res; +import bisq.core.offer.Offer; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.trade.Trade; +import bisq.core.user.Preferences; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.util.Tuple5; + +import org.apache.commons.lang3.StringUtils; + +import javafx.scene.paint.Color; + +import java.util.Date; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static bisq.desktop.util.Colors.AVATAR_BLUE; +import static bisq.desktop.util.Colors.AVATAR_GREEN; +import static bisq.desktop.util.Colors.AVATAR_ORANGE; +import static bisq.desktop.util.Colors.AVATAR_RED; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class PeerInfoIconTrading extends PeerInfoIcon { + private final AccountAgeWitnessService accountAgeWitnessService; + private boolean isFiatCurrency; + + public PeerInfoIconTrading(NodeAddress nodeAddress, + String role, + int numTrades, + PrivateNotificationManager privateNotificationManager, + Offer offer, + Preferences preferences, + AccountAgeWitnessService accountAgeWitnessService, + boolean useDevPrivilegeKeys) { + this(nodeAddress, + role, + numTrades, + privateNotificationManager, + offer, + null, + preferences, + accountAgeWitnessService, + useDevPrivilegeKeys); + } + + public PeerInfoIconTrading(NodeAddress nodeAddress, + String role, + int numTrades, + PrivateNotificationManager privateNotificationManager, + Trade Trade, + Preferences preferences, + AccountAgeWitnessService accountAgeWitnessService, + boolean useDevPrivilegeKeys) { + this(nodeAddress, + role, + numTrades, + privateNotificationManager, + Trade.getOffer(), + Trade, + preferences, + accountAgeWitnessService, + useDevPrivilegeKeys); + } + + private PeerInfoIconTrading(NodeAddress nodeAddress, + String role, + int numTrades, + PrivateNotificationManager privateNotificationManager, + @Nullable Offer offer, + @Nullable Trade trade, + Preferences preferences, + AccountAgeWitnessService accountAgeWitnessService, + boolean useDevPrivilegeKeys) { + super(nodeAddress, preferences); + this.numTrades = numTrades; + this.accountAgeWitnessService = accountAgeWitnessService; + if (offer == null) { + checkNotNull(trade, "Trade must not be null if offer is null."); + offer = trade.getOffer(); + } + checkNotNull(offer, "Offer must not be null"); + isFiatCurrency = offer.isFiatOffer(); + initialize(role, offer, trade, privateNotificationManager, useDevPrivilegeKeys); + } + + protected void initialize(String role, + Offer offer, + Trade trade, + PrivateNotificationManager privateNotificationManager, + boolean useDevPrivilegeKeys) { + boolean hasTraded = numTrades > 0; + Tuple5 peersAccount = getPeersAccountAge(trade, offer); + + Long accountAge = peersAccount.first; + Long signAge = peersAccount.second; + + tooltipText = hasTraded ? + Res.get("peerInfoIcon.tooltip.trade.traded", role, fullAddress, numTrades, getAccountAgeTooltip(accountAge)) : + Res.get("peerInfoIcon.tooltip.trade.notTraded", role, fullAddress, getAccountAgeTooltip(accountAge)); + + createAvatar(getRingColor(offer, trade, accountAge, signAge)); + addMouseListener(numTrades, privateNotificationManager, trade, offer, preferences, useDevPrivilegeKeys, + isFiatCurrency, accountAge, signAge, peersAccount.third, peersAccount.fourth, peersAccount.fifth); + } + + @Override + protected String getAccountAgeTooltip(Long accountAge) { + return isFiatCurrency ? super.getAccountAgeTooltip(accountAge) : ""; + } + + protected Color getRingColor(Offer offer, Trade Trade, Long accountAge, Long signAge) { + // outer circle + // for altcoins we always display green + Color ringColor = AVATAR_GREEN; + if (isFiatCurrency) { + switch (accountAgeWitnessService.getPeersAccountAgeCategory(hasChargebackRisk(Trade, offer) ? signAge : accountAge)) { + case TWO_MONTHS_OR_MORE: + ringColor = AVATAR_GREEN; + break; + case ONE_TO_TWO_MONTHS: + ringColor = AVATAR_BLUE; + break; + case LESS_ONE_MONTH: + ringColor = AVATAR_ORANGE; + break; + case UNVERIFIED: + default: + ringColor = AVATAR_RED; + break; + } + } + return ringColor; + } + + /** + * @param Trade Open trade for trading peer info to be shown + * @param offer Open offer for trading peer info to be shown + * @return account age, sign age, account info, sign info, sign state + */ + private Tuple5 getPeersAccountAge(@Nullable Trade Trade, + @Nullable Offer offer) { + AccountAgeWitnessService.SignState signState = null; + long signAge = -1L; + long accountAge = -1L; + + if (Trade != null) { + offer = Trade.getOffer(); + if (offer == null) { + // unexpected + return new Tuple5<>(signAge, accountAge, Res.get("peerInfo.age.noRisk"), null, null); + } + if (Trade instanceof Trade) { + Trade trade = Trade; + signState = accountAgeWitnessService.getSignState(trade); + signAge = accountAgeWitnessService.getWitnessSignAge(trade, new Date()); + accountAge = accountAgeWitnessService.getAccountAge(trade); + } + } else { + checkNotNull(offer, "Offer must not be null if trade is null."); + signState = accountAgeWitnessService.getSignState(offer); + signAge = accountAgeWitnessService.getWitnessSignAge(offer, new Date()); + accountAge = accountAgeWitnessService.getAccountAge(offer); + } + + if (signState != null && hasChargebackRisk(Trade, offer)) { + String signAgeInfo = Res.get("peerInfo.age.chargeBackRisk"); + String accountSigningState = StringUtils.capitalize(signState.getDisplayString()); + if (signState.equals(AccountAgeWitnessService.SignState.UNSIGNED)) { + signAgeInfo = null; + } + + return new Tuple5<>(accountAge, signAge, Res.get("peerInfo.age.noRisk"), signAgeInfo, accountSigningState); + } + return new Tuple5<>(accountAge, signAge, Res.get("peerInfo.age.noRisk"), null, null); + } + + private static boolean hasChargebackRisk(@Nullable Trade Trade, @Nullable Offer offer) { + Offer offerToCheck = Trade != null ? Trade.getOffer() : offer; + + return offerToCheck != null && + PaymentMethod.hasChargebackRisk(offerToCheck.getPaymentMethod(), offerToCheck.getCurrencyCode()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/SimpleMarkdownLabel.java b/desktop/src/main/java/bisq/desktop/components/SimpleMarkdownLabel.java new file mode 100644 index 0000000000..1361229336 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/SimpleMarkdownLabel.java @@ -0,0 +1,61 @@ +/* + * 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 . + */ + +package bisq.desktop.components; + +import bisq.desktop.util.GUIUtil; + +import bisq.core.util.SimpleMarkdownParser; + +import javafx.scene.Node; +import javafx.scene.control.Hyperlink; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; + +import java.util.List; +import java.util.stream.Collectors; + +public class SimpleMarkdownLabel extends TextFlow { + + public SimpleMarkdownLabel(String markdown) { + super(); + getStyleClass().add("markdown-label"); + if (markdown != null) { + updateContent(markdown); + } + } + + public void updateContent(String markdown) { + List items = SimpleMarkdownParser + .parse(markdown) + .stream() + .map(node -> { + if (node instanceof SimpleMarkdownParser.HyperlinkNode) { + var item = ((SimpleMarkdownParser.HyperlinkNode) node); + Hyperlink hyperlink = new Hyperlink(item.getText()); + hyperlink.setOnAction(e -> GUIUtil.openWebPage(item.getHref())); + return hyperlink; + } else { + var item = ((SimpleMarkdownParser.TextNode) node); + return new Text(item.getText()); + } + }) + .collect(Collectors.toList()); + + getChildren().setAll(items); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/TextFieldWithIcon.java b/desktop/src/main/java/bisq/desktop/components/TextFieldWithIcon.java index 2123aa82ff..d2a28028d5 100644 --- a/desktop/src/main/java/bisq/desktop/components/TextFieldWithIcon.java +++ b/desktop/src/main/java/bisq/desktop/components/TextFieldWithIcon.java @@ -29,13 +29,9 @@ import javafx.scene.text.TextAlignment; import javafx.geometry.Pos; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import lombok.Getter; public class TextFieldWithIcon extends AnchorPane { - public static final Logger log = LoggerFactory.getLogger(TextFieldWithIcon.class); @Getter private final Label iconLabel; @Getter @@ -45,7 +41,6 @@ public class TextFieldWithIcon extends AnchorPane { public TextFieldWithIcon() { textField = new JFXTextField(); textField.setEditable(false); - textField.setMouseTransparent(true); textField.setFocusTraversable(false); setLeftAnchor(textField, 0d); setRightAnchor(textField, 0d); diff --git a/desktop/src/main/java/bisq/desktop/components/TitledGroupBg.java b/desktop/src/main/java/bisq/desktop/components/TitledGroupBg.java index c83c5c0e17..4b933eb05b 100644 --- a/desktop/src/main/java/bisq/desktop/components/TitledGroupBg.java +++ b/desktop/src/main/java/bisq/desktop/components/TitledGroupBg.java @@ -17,19 +17,29 @@ package bisq.desktop.components; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; + +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; + import javafx.scene.control.Label; import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; +import javafx.scene.text.Text; import javafx.geometry.Insets; +import javafx.geometry.Pos; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; public class TitledGroupBg extends Pane { + private final HBox box; private final Label label; private final StringProperty text = new SimpleStringProperty(); + private Text helpIcon; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -39,13 +49,18 @@ public class TitledGroupBg extends Pane { GridPane.setMargin(this, new Insets(-10, -10, -10, -10)); GridPane.setColumnSpan(this, 2); + box = new HBox(); + box.setSpacing(4); + box.setLayoutX(4); + box.setLayoutY(-8); + box.setPadding(new Insets(0, 7, 0, 5)); + box.setAlignment(Pos.CENTER_LEFT); + label = new AutoTooltipLabel(); label.textProperty().bind(text); - label.setLayoutX(4); - label.setLayoutY(-8); - label.setPadding(new Insets(0, 7, 0, 5)); setActive(); - getChildren().add(label); + box.getChildren().add(label); + getChildren().add(box); } public void setInactive() { @@ -65,10 +80,6 @@ public class TitledGroupBg extends Pane { label.getStyleClass().add("titled-group-bg-label-active"); } - public String getText() { - return text.get(); - } - public StringProperty textProperty() { return text; } @@ -77,8 +88,13 @@ public class TitledGroupBg extends Pane { this.text.set(text); } - public Label getLabel() { - return label; - } + public void setHelpUrl(String helpUrl) { + if (helpIcon == null) { + helpIcon = FormBuilder.getIcon(MaterialDesignIcon.HELP_CIRCLE_OUTLINE, "1em"); + helpIcon.getStyleClass().addAll("icon", "link-icon"); + box.getChildren().add(helpIcon); + } + helpIcon.setOnMouseClicked(e -> GUIUtil.openWebPage(helpUrl)); + } } diff --git a/desktop/src/main/java/bisq/desktop/components/TxConfidenceListItem.java b/desktop/src/main/java/bisq/desktop/components/TxConfidenceListItem.java deleted file mode 100644 index d5bccd9c33..0000000000 --- a/desktop/src/main/java/bisq/desktop/components/TxConfidenceListItem.java +++ /dev/null @@ -1,72 +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 . - */ - -package bisq.desktop.components; - -import bisq.desktop.components.indicator.TxConfidenceIndicator; -import bisq.desktop.util.GUIUtil; - -import bisq.core.btc.listeners.TxConfidenceListener; - -import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionConfidence; - -import javafx.scene.control.Tooltip; - -import lombok.Data; - - -@Data -public class TxConfidenceListItem { - protected final String txId; - protected int confirmations = 0; - protected TxConfidenceIndicator txConfidenceIndicator; - protected TxConfidenceListener txConfidenceListener; - - protected TxConfidenceListItem(Transaction transaction) { - - txId = transaction.getTxId().toString(); - txConfidenceIndicator = new TxConfidenceIndicator(); - txConfidenceIndicator.setId("funds-confidence"); - Tooltip tooltip = new Tooltip(); - txConfidenceIndicator.setProgress(0); - txConfidenceIndicator.setPrefSize(24, 24); - txConfidenceIndicator.setTooltip(tooltip); - - txConfidenceListener = new TxConfidenceListener(txId) { - @Override - public void onTransactionConfidenceChanged(TransactionConfidence confidence) { - updateConfidence(confidence, tooltip); - } - }; - } - - protected TxConfidenceListItem() { - this.txId = null; - } - - private void updateConfidence(TransactionConfidence confidence, Tooltip tooltip) { - if (confidence != null) { - GUIUtil.updateConfidence(confidence, tooltip, txConfidenceIndicator); - confirmations = confidence.getDepthInBlocks(); - } - } - - public void cleanup() { - } -} - diff --git a/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java b/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java index ce8b7dd8b2..90a26a35ef 100644 --- a/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java +++ b/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java @@ -46,18 +46,11 @@ import lombok.Setter; import javax.annotation.Nullable; public class TxIdTextField extends AnchorPane { + @Setter private static Preferences preferences; - - public static void setPreferences(Preferences preferences) { - TxIdTextField.preferences = preferences; - } - + @Setter private static BtcWalletService walletService; - public static void setWalletService(BtcWalletService walletService) { - TxIdTextField.walletService = walletService; - } - @Getter private final TextField textField; private final Tooltip progressIndicatorTooltip; @@ -65,7 +58,6 @@ public class TxIdTextField extends AnchorPane { private final Label copyIcon, blockExplorerIcon, missingTxWarningIcon; private TxConfidenceListener txConfidenceListener; - /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java index 9d191f4995..0b695184b0 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java @@ -22,6 +22,7 @@ import bisq.desktop.common.model.ActivatableDataModel; import java.time.Instant; import java.time.temporal.TemporalAdjuster; +import java.util.Comparator; import java.util.Map; import java.util.function.BinaryOperator; import java.util.function.Predicate; diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java new file mode 100644 index 0000000000..92e5285d6d --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java @@ -0,0 +1,811 @@ +/* + * 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 . + */ + +package bisq.desktop.components.chart; + +import bisq.desktop.common.view.ActivatableViewAndModel; +import bisq.desktop.components.AutoTooltipSlideToggleButton; +import bisq.desktop.components.AutoTooltipToggleButton; + +import bisq.core.locale.Res; + +import bisq.common.UserThread; + +import javafx.stage.PopupWindow; +import javafx.stage.Stage; + +import javafx.scene.Node; +import javafx.scene.chart.Axis; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Label; +import javafx.scene.control.SplitPane; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.control.Tooltip; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +import javafx.geometry.Bounds; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.Side; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; + +import javafx.event.EventHandler; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import javafx.util.Duration; + +import java.time.temporal.TemporalAdjuster; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public abstract class ChartView> extends ActivatableViewAndModel { + private Pane center; + private SplitPane timelineNavigation; + protected NumberAxis xAxis, yAxis; + protected LineChart chart; + private HBox timelineLabels, legendBox2, legendBox3; + private final ToggleGroup timeIntervalToggleGroup = new ToggleGroup(); + + protected final Set> activeSeries = new HashSet<>(); + protected final Map seriesIndexMap = new HashMap<>(); + protected final Map legendToggleBySeriesName = new HashMap<>(); + private final List dividerNodes = new ArrayList<>(); + private final List dividerNodesTooltips = new ArrayList<>(); + private ChangeListener widthListener; + private ChangeListener timeIntervalChangeListener; + private ListChangeListener nodeListChangeListener; + private int maxSeriesSize; + private boolean centerPanePressed; + private double x; + + @Setter + protected boolean isRadioButtonBehaviour; + @Setter + private int maxDataPointsForShowingSymbols = 100; + private ChangeListener yAxisWidthListener; + private EventHandler dividerMouseDraggedEventHandler; + private final StringProperty fromProperty = new SimpleStringProperty(); + private final StringProperty toProperty = new SimpleStringProperty(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public ChartView(T model) { + super(model); + + root = new VBox(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void initialize() { + // We need to call prepareInitialize as we are not using FXMLLoader + prepareInitialize(); + + maxSeriesSize = 0; + centerPanePressed = false; + x = 0; + + // Series + createSeries(); + + // Time interval + HBox timeIntervalBox = getTimeIntervalBox(); + + // chart + xAxis = getXAxis(); + yAxis = getYAxis(); + chart = getChart(); + + // Timeline navigation + addTimelineNavigation(); + + // Legend + HBox legendBox1 = initLegendsAndGetLegendBox(getSeriesForLegend1()); + + Collection> seriesForLegend2 = getSeriesForLegend2(); + if (seriesForLegend2 != null && !seriesForLegend2.isEmpty()) { + legendBox2 = initLegendsAndGetLegendBox(seriesForLegend2); + } + + Collection> seriesForLegend3 = getSeriesForLegend3(); + if (seriesForLegend3 != null && !seriesForLegend3.isEmpty()) { + legendBox3 = initLegendsAndGetLegendBox(seriesForLegend3); + } + + // Set active series/legends + defineAndAddActiveSeries(); + + // Put all together + VBox timelineNavigationBox = new VBox(); + double paddingLeft = 15; + double paddingRight = 89; + // Y-axis width depends on data so we register a listener to get correct value + yAxisWidthListener = (observable, oldValue, newValue) -> { + double width = newValue.doubleValue(); + if (width > 0) { + double rightPadding = width + 14; + VBox.setMargin(timeIntervalBox, new Insets(0, rightPadding, 0, paddingLeft)); + VBox.setMargin(timelineNavigation, new Insets(0, rightPadding, 0, paddingLeft)); + VBox.setMargin(timelineLabels, new Insets(0, rightPadding, 0, paddingLeft)); + VBox.setMargin(legendBox1, new Insets(10, rightPadding, 0, paddingLeft)); + if (legendBox2 != null) { + VBox.setMargin(legendBox2, new Insets(-20, rightPadding, 0, paddingLeft)); + } + if (legendBox3 != null) { + VBox.setMargin(legendBox3, new Insets(-20, rightPadding, 0, paddingLeft)); + } + + if (model.getDividerPositions()[0] == 0 && model.getDividerPositions()[1] == 1) { + resetTimeNavigation(); + } + } + }; + + VBox.setMargin(timeIntervalBox, new Insets(0, paddingRight, 0, paddingLeft)); + VBox.setMargin(timelineNavigation, new Insets(0, paddingRight, 0, paddingLeft)); + VBox.setMargin(timelineLabels, new Insets(0, paddingRight, 0, paddingLeft)); + VBox.setMargin(legendBox1, new Insets(0, paddingRight, 0, paddingLeft)); + timelineNavigationBox.getChildren().addAll(timelineNavigation, timelineLabels, legendBox1); + if (legendBox2 != null) { + VBox.setMargin(legendBox2, new Insets(-20, paddingRight, 0, paddingLeft)); + timelineNavigationBox.getChildren().add(legendBox2); + } + if (legendBox3 != null) { + VBox.setMargin(legendBox3, new Insets(-20, paddingRight, 0, paddingLeft)); + timelineNavigationBox.getChildren().add(legendBox3); + } + root.getChildren().addAll(timeIntervalBox, chart, timelineNavigationBox); + + // Listeners + widthListener = (observable, oldValue, newValue) -> { + timelineNavigation.setDividerPosition(0, model.getDividerPositions()[0]); + timelineNavigation.setDividerPosition(1, model.getDividerPositions()[1]); + }; + + timeIntervalChangeListener = (observable, oldValue, newValue) -> { + if (newValue != null) { + onTimeIntervalChanged(newValue); + } + }; + + nodeListChangeListener = c -> { + while (c.next()) { + if (c.wasAdded()) { + c.getAddedSubList().stream() + .filter(node -> node instanceof Text) + .forEach(node -> node.getStyleClass().add("axis-tick-mark-text-node")); + } + } + }; + } + + @Override + public void activate() { + timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); + + TemporalAdjuster temporalAdjuster = model.getTemporalAdjuster(); + applyTemporalAdjuster(temporalAdjuster); + findTimeIntervalToggleByTemporalAdjuster(temporalAdjuster).ifPresent(timeIntervalToggleGroup::selectToggle); + + defineAndAddActiveSeries(); + initBoundsForTimelineNavigation(); + + // Apply listeners and handlers + root.widthProperty().addListener(widthListener); + xAxis.getChildrenUnmodifiable().addListener(nodeListChangeListener); + yAxis.widthProperty().addListener(yAxisWidthListener); + timeIntervalToggleGroup.selectedToggleProperty().addListener(timeIntervalChangeListener); + + timelineNavigation.setOnMousePressed(this::onMousePressedSplitPane); + timelineNavigation.setOnMouseDragged(this::onMouseDragged); + center.setOnMousePressed(this::onMousePressedCenter); + center.setOnMouseReleased(this::onMouseReleasedCenter); + + addLegendToggleActionHandlers(getSeriesForLegend1()); + addLegendToggleActionHandlers(getSeriesForLegend2()); + addLegendToggleActionHandlers(getSeriesForLegend3()); + addActionHandlersToDividers(); + } + + @Override + public void deactivate() { + root.widthProperty().removeListener(widthListener); + xAxis.getChildrenUnmodifiable().removeListener(nodeListChangeListener); + yAxis.widthProperty().removeListener(yAxisWidthListener); + timeIntervalToggleGroup.selectedToggleProperty().removeListener(timeIntervalChangeListener); + + timelineNavigation.setOnMousePressed(null); + timelineNavigation.setOnMouseDragged(null); + center.setOnMousePressed(null); + center.setOnMouseReleased(null); + + removeLegendToggleActionHandlers(getSeriesForLegend1()); + removeLegendToggleActionHandlers(getSeriesForLegend2()); + removeLegendToggleActionHandlers(getSeriesForLegend3()); + removeActionHandlersToDividers(); + + // clear data, reset states. We keep timeInterval state though + activeSeries.clear(); + chart.getData().clear(); + legendToggleBySeriesName.values().forEach(e -> e.setSelected(false)); + dividerNodes.clear(); + dividerNodesTooltips.clear(); + model.invalidateCache(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TimeInterval/TemporalAdjuster + /////////////////////////////////////////////////////////////////////////////////////////// + + protected HBox getTimeIntervalBox() { + ToggleButton year = getTimeIntervalToggleButton(Res.get("time.year"), TemporalAdjusterModel.Interval.YEAR, + timeIntervalToggleGroup, "toggle-left"); + ToggleButton halfYear = getTimeIntervalToggleButton(Res.get("time.halfYear"), TemporalAdjusterModel.Interval.HALF_YEAR, + timeIntervalToggleGroup, "toggle-center"); + ToggleButton quarter = getTimeIntervalToggleButton(Res.get("time.quarter"), TemporalAdjusterModel.Interval.QUARTER, + timeIntervalToggleGroup, "toggle-center"); + ToggleButton month = getTimeIntervalToggleButton(Res.get("time.month"), TemporalAdjusterModel.Interval.MONTH, + timeIntervalToggleGroup, "toggle-center"); + ToggleButton week = getTimeIntervalToggleButton(Res.get("time.week"), TemporalAdjusterModel.Interval.WEEK, + timeIntervalToggleGroup, "toggle-center"); + ToggleButton day = getTimeIntervalToggleButton(Res.get("time.day"), TemporalAdjusterModel.Interval.DAY, + timeIntervalToggleGroup, "toggle-center"); + HBox toggleBox = new HBox(); + toggleBox.setSpacing(0); + toggleBox.setAlignment(Pos.CENTER_LEFT); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + toggleBox.getChildren().addAll(spacer, year, halfYear, quarter, month, week, day); + return toggleBox; + } + + private ToggleButton getTimeIntervalToggleButton(String label, + TemporalAdjusterModel.Interval interval, + ToggleGroup toggleGroup, + String style) { + ToggleButton toggleButton = new AutoTooltipToggleButton(label); + toggleButton.setUserData(interval); + toggleButton.setToggleGroup(toggleGroup); + toggleButton.setId(style); + return toggleButton; + } + + protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { + model.applyTemporalAdjuster(temporalAdjuster); + findTimeIntervalToggleByTemporalAdjuster(temporalAdjuster) + .map(e -> (TemporalAdjusterModel.Interval) e.getUserData()) + .ifPresent(model::setDateFormatPattern); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart + /////////////////////////////////////////////////////////////////////////////////////////// + + protected NumberAxis getXAxis() { + NumberAxis xAxis = new NumberAxis(); + xAxis.setForceZeroInRange(false); + xAxis.setAutoRanging(true); + xAxis.setTickLabelFormatter(model.getTimeAxisStringConverter()); + return xAxis; + } + + protected NumberAxis getYAxis() { + NumberAxis yAxis = new NumberAxis(); + yAxis.setForceZeroInRange(true); + yAxis.setSide(Side.RIGHT); + yAxis.setTickLabelFormatter(model.getYAxisStringConverter()); + return yAxis; + } + + // Add implementation if update of the y axis is required at series change + protected void onSetYAxisFormatter(XYChart.Series series) { + } + + protected LineChart getChart() { + LineChart chart = new LineChart<>(xAxis, yAxis); + chart.setAnimated(false); + chart.setLegendVisible(false); + chart.setMinHeight(200); + chart.setId("charts-dao"); + return chart; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Legend + /////////////////////////////////////////////////////////////////////////////////////////// + + protected HBox initLegendsAndGetLegendBox(Collection> collection) { + HBox hBox = new HBox(); + hBox.setSpacing(10); + collection.forEach(series -> { + AutoTooltipSlideToggleButton toggle = new AutoTooltipSlideToggleButton(); + toggle.setMinWidth(200); + toggle.setAlignment(Pos.TOP_LEFT); + String seriesId = getSeriesId(series); + legendToggleBySeriesName.put(seriesId, toggle); + toggle.setText(seriesId); + toggle.setId("charts-legend-toggle" + seriesIndexMap.get(seriesId)); + toggle.setSelected(false); + hBox.getChildren().add(toggle); + }); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + hBox.getChildren().add(spacer); + return hBox; + } + + private void addLegendToggleActionHandlers(@Nullable Collection> collection) { + if (collection != null) { + collection.forEach(series -> + legendToggleBySeriesName.get(getSeriesId(series)).setOnAction(e -> onSelectLegendToggle(series))); + } + } + + private void removeLegendToggleActionHandlers(@Nullable Collection> collection) { + if (collection != null) { + collection.forEach(series -> + legendToggleBySeriesName.get(getSeriesId(series)).setOnAction(null)); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Timeline navigation + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addTimelineNavigation() { + Pane left = new Pane(); + center = new Pane(); + center.setId("chart-navigation-center-pane"); + Pane right = new Pane(); + timelineNavigation = new SplitPane(left, center, right); + timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); + timelineNavigation.setMinHeight(25); + timelineLabels = new HBox(); + } + + // After initial chart data are created we apply the text from the x-axis ticks to our timeline navigation. + protected void applyTimeLineNavigationLabels() { + timelineLabels.getChildren().clear(); + ObservableList> tickMarks = xAxis.getTickMarks(); + int size = tickMarks.size(); + for (int i = 0; i < size; i++) { + Axis.TickMark tickMark = tickMarks.get(i); + Number xValue = tickMark.getValue(); + String xValueString; + if (xAxis.getTickLabelFormatter() != null) { + xValueString = xAxis.getTickLabelFormatter().toString(xValue); + } else { + xValueString = String.valueOf(xValue); + } + Label label = new Label(xValueString); + label.setMinHeight(30); + label.setId("chart-navigation-label"); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + if (i < size - 1) { + timelineLabels.getChildren().addAll(label, spacer); + } else { + // After last label we don't add a spacer + timelineLabels.getChildren().add(label); + } + } + } + + private void onMousePressedSplitPane(MouseEvent e) { + x = e.getX(); + applyFromToDates(); + showDividerTooltips(); + } + + private void onMousePressedCenter(MouseEvent e) { + centerPanePressed = true; + applyFromToDates(); + showDividerTooltips(); + } + + private void onMouseReleasedCenter(MouseEvent e) { + centerPanePressed = false; + onTimelineChanged(); + hideDividerTooltips(); + } + + private void onMouseDragged(MouseEvent e) { + if (centerPanePressed) { + double newX = e.getX(); + double width = timelineNavigation.getWidth(); + double relativeDelta = (x - newX) / width; + double leftPos = timelineNavigation.getDividerPositions()[0] - relativeDelta; + double rightPos = timelineNavigation.getDividerPositions()[1] - relativeDelta; + + // Model might limit application of new values if we hit a boundary + model.onTimelineMouseDrag(leftPos, rightPos); + timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); + x = newX; + + applyFromToDates(); + showDividerTooltips(); + } + } + + private void addActionHandlersToDividers() { + // No API access to dividers ;-( only via css lookup hack (https://stackoverflow.com/questions/40707295/how-to-add-listener-to-divider-position?rq=1) + // Need to be done after added to scene and call requestLayout and applyCss. We keep it in a list atm + // and set action handler in activate. + timelineNavigation.requestLayout(); + timelineNavigation.applyCss(); + dividerMouseDraggedEventHandler = event -> { + applyFromToDates(); + showDividerTooltips(); + }; + + for (Node node : timelineNavigation.lookupAll(".split-pane-divider")) { + dividerNodes.add(node); + node.setOnMouseReleased(e -> { + hideDividerTooltips(); + onTimelineChanged(); + }); + node.addEventHandler(MouseEvent.MOUSE_DRAGGED, dividerMouseDraggedEventHandler); + + Tooltip tooltip = new Tooltip(""); + dividerNodesTooltips.add(tooltip); + tooltip.setShowDelay(Duration.millis(300)); + tooltip.setShowDuration(Duration.seconds(3)); + tooltip.textProperty().bind(dividerNodes.size() == 1 ? fromProperty : toProperty); + Tooltip.install(node, tooltip); + } + } + + private void removeActionHandlersToDividers() { + dividerNodes.forEach(node -> { + node.setOnMouseReleased(null); + node.removeEventHandler(MouseEvent.MOUSE_DRAGGED, dividerMouseDraggedEventHandler); + }); + for (int i = 0; i < dividerNodesTooltips.size(); i++) { + Tooltip tooltip = dividerNodesTooltips.get(i); + tooltip.textProperty().unbind(); + Tooltip.uninstall(dividerNodes.get(i), tooltip); + } + } + + private void resetTimeNavigation() { + timelineNavigation.setDividerPositions(0d, 1d); + model.onTimelineNavigationChanged(0, 1); + } + + private void showDividerTooltips() { + showDividerTooltip(0); + showDividerTooltip(1); + } + + private void hideDividerTooltips() { + dividerNodesTooltips.forEach(PopupWindow::hide); + } + + private void showDividerTooltip(int index) { + Node divider = dividerNodes.get(index); + Bounds bounds = divider.localToScene(divider.getBoundsInLocal()); + Tooltip tooltip = dividerNodesTooltips.get(index); + double xOffset = index == 0 ? -90 : 10; + Stage stage = (Stage) root.getScene().getWindow(); + tooltip.show(stage, stage.getX() + bounds.getMaxX() + xOffset, + stage.getY() + bounds.getMaxY() - 40); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Series + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract void createSeries(); + + protected abstract Collection> getSeriesForLegend1(); + + // If a second legend is used this has to be overridden + protected Collection> getSeriesForLegend2() { + return null; + } + + protected Collection> getSeriesForLegend3() { + return null; + } + + protected abstract void defineAndAddActiveSeries(); + + protected void activateSeries(XYChart.Series series) { + if (activeSeries.contains(series)) { + return; + } + + chart.getData().add(series); + activeSeries.add(series); + legendToggleBySeriesName.get(getSeriesId(series)).setSelected(true); + applyDataAndUpdate(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract CompletableFuture applyData(); + + private void applyDataAndUpdate() { + long ts = System.currentTimeMillis(); + applyData().whenComplete((r, t) -> { + log.debug("applyData took {}", System.currentTimeMillis() - ts); + long ts2 = System.currentTimeMillis(); + updateChartAfterDataChange(); + log.debug("updateChartAfterDataChange took {}", System.currentTimeMillis() - ts2); + + onDataApplied(); + }); + } + + /** + * Implementations define which series will be used for setBoundsForTimelineNavigation + */ + protected abstract void initBoundsForTimelineNavigation(); + + /** + * @param data The series data which determines the min/max x values for the time line navigation. + * If not applicable initBoundsForTimelineNavigation requires custom implementation. + */ + protected void setBoundsForTimelineNavigation(ObservableList> data) { + model.initBounds(data); + xAxis.setLowerBound(model.getLowerBound().doubleValue()); + xAxis.setUpperBound(model.getUpperBound().doubleValue()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Handlers triggering a data/chart update + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onTimeIntervalChanged(Toggle newValue) { + TemporalAdjusterModel.Interval interval = (TemporalAdjusterModel.Interval) newValue.getUserData(); + applyTemporalAdjuster(interval.getAdjuster()); + model.invalidateCache(); + applyDataAndUpdate(); + } + + private void onTimelineChanged() { + updateTimeLinePositions(); + + model.invalidateCache(); + applyDataAndUpdate(); + } + + private void updateTimeLinePositions() { + double leftPos = timelineNavigation.getDividerPositions()[0]; + double rightPos = timelineNavigation.getDividerPositions()[1]; + model.onTimelineNavigationChanged(leftPos, rightPos); + // We need to update as model might have adjusted the values + timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); + fromProperty.set(model.getTimeAxisStringConverter().toString(model.getFromDate()).replace("\n", " ")); + toProperty.set(model.getTimeAxisStringConverter().toString(model.getToDate()).replace("\n", " ")); + } + + private void applyFromToDates() { + double leftPos = timelineNavigation.getDividerPositions()[0]; + double rightPos = timelineNavigation.getDividerPositions()[1]; + model.applyFromToDates(leftPos, rightPos); + fromProperty.set(model.getTimeAxisStringConverter().toString(model.getFromDate()).replace("\n", " ")); + toProperty.set(model.getTimeAxisStringConverter().toString(model.getToDate()).replace("\n", " ")); + } + + private void onSelectLegendToggle(XYChart.Series series) { + boolean isSelected = legendToggleBySeriesName.get(getSeriesId(series)).isSelected(); + // If we have set that flag we deselect all other toggles + if (isRadioButtonBehaviour) { + new ArrayList<>(chart.getData()).stream() // We need to copy to a new list to avoid ConcurrentModificationException + .filter(activeSeries::contains) + .forEach(seriesToRemove -> { + chart.getData().remove(seriesToRemove); + String seriesId = getSeriesId(seriesToRemove); + activeSeries.remove(seriesToRemove); + legendToggleBySeriesName.get(seriesId).setSelected(false); + }); + } + + if (isSelected) { + chart.getData().add(series); + activeSeries.add(series); + applyDataAndUpdate(); + + if (isRadioButtonBehaviour) { + // We support different y-axis formats only if isRadioButtonBehaviour is set, otherwise we would get + // mixed data on y-axis + onSetYAxisFormatter(series); + } + } else if (!isRadioButtonBehaviour) { // if isRadioButtonBehaviour we have removed it already via the code above + chart.getData().remove(series); + activeSeries.remove(series); + updateChartAfterDataChange(); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart update after data change + /////////////////////////////////////////////////////////////////////////////////////////// + + // Update of the chart data can be triggered by: + // 1. activate() + // 2. TimeInterval toggle change + // 3. Timeline navigation change + // 4. Legend/series toggle change + + // Timeline navigation and legend/series toggles get reset at activate. + // Time interval toggle keeps its state at screen changes. + protected void updateChartAfterDataChange() { + // If a series got no data points after update we need to clear it from the chart + cleanupDanglingSeries(); + + // Hides symbols if too many data points are created + updateSymbolsVisibility(); + + // When series gets added/removed the JavaFx charts framework would try to apply styles by the index of + // addition, but we want to use a static color assignment which is synced with the legend color. + applySeriesStyles(); + + // Set tooltip on symbols + applyTooltip(); + } + + private void cleanupDanglingSeries() { + List> activeSeriesList = new ArrayList<>(activeSeries); + activeSeriesList.forEach(series -> { + ObservableList> seriesOnChart = chart.getData(); + if (series.getData().isEmpty()) { + seriesOnChart.remove(series); + } else if (!seriesOnChart.contains(series)) { + seriesOnChart.add(series); + } + }); + } + + private void updateSymbolsVisibility() { + maxDataPointsForShowingSymbols = 100; + long numDataPoints = chart.getData().stream() + .map(XYChart.Series::getData) + .mapToLong(List::size) + .max() + .orElse(0); + boolean prevValue = chart.getCreateSymbols(); + boolean newValue = numDataPoints < maxDataPointsForShowingSymbols; + if (prevValue != newValue) { + chart.setCreateSymbols(newValue); + } + } + + // The chart framework assigns the colored depending on the order it got added, but want to keep colors + // the same so they match with the legend toggle. + private void applySeriesStyles() { + for (int index = 0; index < chart.getData().size(); index++) { + XYChart.Series series = chart.getData().get(index); + int staticIndex = seriesIndexMap.get(getSeriesId(series)); + Set lines = getNodesForStyle(series.getNode(), ".default-color%d.chart-series-line"); + Stream symbols = series.getData().stream().map(XYChart.Data::getNode) + .flatMap(node -> getNodesForStyle(node, ".default-color%d.chart-line-symbol").stream()); + Stream.concat(lines.stream(), symbols).forEach(node -> { + removeStyles(node); + node.getStyleClass().add("default-color" + staticIndex); + }); + } + } + + private void applyTooltip() { + chart.getData().forEach(series -> { + series.getData().forEach(data -> { + Node node = data.getNode(); + if (node == null) { + return; + } + String xValue = model.getTooltipDateConverter(data.getXValue()); + String yValue = model.getYAxisStringConverter().toString(data.getYValue()); + Tooltip.install(node, new Tooltip(Res.get("dao.factsAndFigures.supply.chart.tradeFee.toolTip", yValue, xValue))); + }); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private void removeStyles(Node node) { + for (int i = 0; i < getMaxSeriesSize(); i++) { + node.getStyleClass().remove("default-color" + i); + } + } + + private Set getNodesForStyle(Node node, String style) { + Set result = new HashSet<>(); + if (node != null) { + for (int i = 0; i < getMaxSeriesSize(); i++) { + result.addAll(node.lookupAll(String.format(style, i))); + } + } + return result; + } + + private int getMaxSeriesSize() { + maxSeriesSize = Math.max(maxSeriesSize, chart.getData().size()); + return maxSeriesSize; + } + + private Optional findTimeIntervalToggleByTemporalAdjuster(TemporalAdjuster adjuster) { + return timeIntervalToggleGroup.getToggles().stream() + .filter(toggle -> ((TemporalAdjusterModel.Interval) toggle.getUserData()).getAdjuster().equals(adjuster)) + .findAny(); + } + + // We use the name as id as there is no other suitable data inside series + protected String getSeriesId(XYChart.Series series) { + return series.getName(); + } + + protected void mapToUserThread(Runnable command) { + UserThread.execute(command); + } + + protected void onDataApplied() { + // Once we have data applied we need to call initBoundsForTimelineNavigation again + if (model.upperBound.longValue() == 0) { + initBoundsForTimelineNavigation(); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java index f9bea38cdd..931e4da132 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java @@ -79,6 +79,8 @@ public abstract class ChartViewModel extends Activatab case YEAR: dateFormatPatters = "yyyy"; break; + case HALF_YEAR: + case QUARTER: case MONTH: dateFormatPatters = "MMM\nyyyy"; break; @@ -247,8 +249,9 @@ public abstract class ChartViewModel extends Activatab private Tuple2 getMinMax(List> chartData) { long min = Long.MAX_VALUE, max = 0; for (XYChart.Data data : chartData) { - min = Math.min(data.getXValue().longValue(), min); - max = Math.max(data.getXValue().longValue(), max); + long value = data.getXValue().longValue(); + min = Math.min(value, min); + max = Math.max(value, max); } return new Tuple2<>((double) min, (double) max); } diff --git a/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java b/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java index b72d8f9f61..cbc2c82d2a 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java @@ -17,21 +17,58 @@ package bisq.desktop.components.chart; +import bisq.common.util.MathUtils; + import java.time.DayOfWeek; import java.time.Instant; +import java.time.LocalDate; import java.time.ZoneId; import java.time.temporal.TemporalAdjuster; import java.time.temporal.TemporalAdjusters; +import java.math.RoundingMode; + import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import static java.time.temporal.ChronoField.DAY_OF_YEAR; + @Slf4j public class TemporalAdjusterModel { private static final ZoneId ZONE_ID = ZoneId.systemDefault(); public enum Interval { YEAR(TemporalAdjusters.firstDayOfYear()), + HALF_YEAR(temporal -> { + long halfYear = temporal.range(DAY_OF_YEAR).getMaximum() / 2; + int dayOfYear = 0; + if (temporal instanceof LocalDate) { + dayOfYear = ((LocalDate) temporal).getDayOfYear(); // getDayOfYear delivers 1-365 (366 in leap years) + } + if (dayOfYear <= halfYear) { + return temporal.with(DAY_OF_YEAR, 1); + } else { + return temporal.with(DAY_OF_YEAR, halfYear + 1); + } + }), + QUARTER(temporal -> { + long quarter1 = temporal.range(DAY_OF_YEAR).getMaximum() / 4; + long halfYear = temporal.range(DAY_OF_YEAR).getMaximum() / 2; + long quarter3 = MathUtils.roundDoubleToLong(temporal.range(DAY_OF_YEAR).getMaximum() * 0.75, RoundingMode.FLOOR); + int dayOfYear = 0; + if (temporal instanceof LocalDate) { + dayOfYear = ((LocalDate) temporal).getDayOfYear(); + } + if (dayOfYear <= quarter1) { + return temporal.with(DAY_OF_YEAR, 1); + } else if (dayOfYear <= halfYear) { + return temporal.with(DAY_OF_YEAR, quarter1 + 1); + } else if (dayOfYear <= quarter3) { + return temporal.with(DAY_OF_YEAR, halfYear + 1); + } else { + return temporal.with(DAY_OF_YEAR, quarter3 + 1); + } + }), MONTH(TemporalAdjusters.firstDayOfMonth()), WEEK(TemporalAdjusters.next(DayOfWeek.MONDAY)), DAY(TemporalAdjusters.ofDateAdjuster(d -> d)); diff --git a/desktop/src/main/java/bisq/desktop/components/controlsfx/control/PopOver.java b/desktop/src/main/java/bisq/desktop/components/controlsfx/control/PopOver.java index 9593cbc430..0b2c8790bd 100644 --- a/desktop/src/main/java/bisq/desktop/components/controlsfx/control/PopOver.java +++ b/desktop/src/main/java/bisq/desktop/components/controlsfx/control/PopOver.java @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2013, ControlsFX +/* + * Copyright (c) 2013, 2016 ControlsFX * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -24,14 +24,26 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + package bisq.desktop.components.controlsfx.control; -import static java.util.Objects.requireNonNull; -import static javafx.scene.input.MouseEvent.MOUSE_CLICKED; import bisq.desktop.components.controlsfx.skin.PopOverSkin; + import javafx.animation.FadeTransition; + +import javafx.stage.Window; +import javafx.stage.WindowEvent; + +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.PopupControl; +import javafx.scene.control.Skin; +import javafx.scene.layout.StackPane; + +import javafx.geometry.Bounds; +import javafx.geometry.Insets; + import javafx.beans.InvalidationListener; -import javafx.beans.Observable; import javafx.beans.WeakInvalidationListener; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; @@ -42,37 +54,47 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; import javafx.beans.value.WeakChangeListener; + import javafx.event.EventHandler; -import javafx.geometry.Bounds; -import javafx.geometry.Insets; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.control.PopupControl; -import javafx.scene.control.Skin; -import javafx.scene.input.MouseEvent; -import javafx.stage.Window; -import javafx.stage.WindowEvent; +import javafx.event.WeakEventHandler; + import javafx.util.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.util.Objects.requireNonNull; +import static javafx.scene.input.MouseEvent.MOUSE_CLICKED; + /** * The PopOver control provides detailed information about an owning node in a * popup window. The popup window has a very lightweight appearance (no default * window decorations) and an arrow pointing at the owner. Due to the nature of * popup windows the PopOver will move around with the parent window when the * user drags it.
- *

+ *
Screenshot of PopOver

* The PopOver can be detached from the owning node by dragging it away from the * owner. It stops displaying an arrow and starts displaying a title and a close * icon.
*
- *

+ *
Screenshot of a detached PopOver

* The following image shows a popover with an accordion content node. PopOver * controls are automatically resizing themselves when the content node changes * its size.
*
- *

+ *
Screenshot of PopOver containing an Accordion

+ * For styling apply stylesheets to the root pane of the PopOver. + * + *

Example:

+ * + *
+ * PopOver popOver = new PopOver();
+ * popOver.getRoot().getStylesheets().add(...);
+ * 
+ * */ public class PopOver extends PopupControl { @@ -84,6 +106,12 @@ public class PopOver extends PopupControl { private double targetY; + private final SimpleBooleanProperty animated = new SimpleBooleanProperty(true); + private final ObjectProperty fadeInDuration = new SimpleObjectProperty<>(DEFAULT_FADE_DURATION); + private final ObjectProperty fadeOutDuration = new SimpleObjectProperty<>(DEFAULT_FADE_DURATION); + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + /** * Creates a pop over with a label as the content node. */ @@ -92,30 +120,24 @@ public class PopOver extends PopupControl { getStyleClass().add(DEFAULT_STYLE_CLASS); + getRoot().getStylesheets().add( + requireNonNull(PopOver.class.getResource("popover.css")).toExternalForm()); //$NON-NLS-1$ + setAnchorLocation(AnchorLocation.WINDOW_TOP_LEFT); - setOnHiding(new EventHandler() { - @Override - public void handle(WindowEvent evt) { - setDetached(false); - } - }); + setOnHiding(evt -> setDetached(false)); /* * Create some initial content. */ - Label label = new Label(""); //$NON-NLS-1$ + Label label = new Label("No content set"); //$NON-NLS-1$ label.setPrefSize(200, 200); label.setPadding(new Insets(4)); setContentNode(label); - ChangeListener repositionListener = new ChangeListener() { - @Override - public void changed(ObservableValue value, - Object oldObject, Object newObject) { - if (isShowing() && !isDetached()) { - show(getOwnerNode(), targetX, targetY); - adjustWindowLocation(); - } + InvalidationListener repositionListener = observable -> { + if (isShowing() && !isDetached()) { + show(getOwnerNode(), targetX, targetY); + adjustWindowLocation(); } }; @@ -123,12 +145,21 @@ public class PopOver extends PopupControl { cornerRadius.addListener(repositionListener); arrowLocation.addListener(repositionListener); arrowIndent.addListener(repositionListener); + headerAlwaysVisible.addListener(repositionListener); + + /* + * A detached popover should of course not automatically hide itself. + */ + detached.addListener(it -> setAutoHide(!isDetached())); + + setAutoHide(true); } /** * Creates a pop over with the given node as the content node. * - * @param content The content shown by the pop over + * @param content + * The content shown by the pop over */ public PopOver(Node content) { this(); @@ -141,9 +172,28 @@ public class PopOver extends PopupControl { return new PopOverSkin(this); } + private final StackPane root = new StackPane(); + + /** + * The root pane stores the content node of the popover. It is accessible + * via this method in order to support proper styling. + * + *

Example:

+ * + *
+     * PopOver popOver = new PopOver();
+     * popOver.getRoot().getStylesheets().add(...);
+     * 
+ * + * @return the root pane + */ + public final StackPane getRoot() { + return root; + } + // Content support. - private final ObjectProperty contentNode = new SimpleObjectProperty( + private final ObjectProperty contentNode = new SimpleObjectProperty<>( this, "contentNode") { //$NON-NLS-1$ @Override public void setValue(Node node) { @@ -151,7 +201,8 @@ public class PopOver extends PopupControl { throw new IllegalArgumentException( "content node can not be null"); //$NON-NLS-1$ } - }; + } + }; /** @@ -186,41 +237,36 @@ public class PopOver extends PopupControl { contentNodeProperty().set(content); } - private InvalidationListener hideListener = new InvalidationListener() { - @Override - public void invalidated(Observable observable) { - if (!isDetached()) { - hide(Duration.ZERO); - } + private final InvalidationListener hideListener = observable -> { + if (!isDetached()) { + hide(Duration.ZERO); } }; - private WeakInvalidationListener weakHideListener = new WeakInvalidationListener( + private final WeakInvalidationListener weakHideListener = new WeakInvalidationListener( hideListener); - private ChangeListener xListener = new ChangeListener() { - @Override - public void changed(ObservableValue value, - Number oldX, Number newX) { - setX(getX() + (newX.doubleValue() - oldX.doubleValue())); + private final ChangeListener xListener = (value, oldX, newX) -> { + if (!isDetached()) { + setAnchorX(getAnchorX() + (newX.doubleValue() - oldX.doubleValue())); } }; - private WeakChangeListener weakXListener = new WeakChangeListener<>( + private final WeakChangeListener weakXListener = new WeakChangeListener<>( xListener); - private ChangeListener yListener = new ChangeListener() { - @Override - public void changed(ObservableValue value, - Number oldY, Number newY) { - setY(getY() + (newY.doubleValue() - oldY.doubleValue())); + private final ChangeListener yListener = (value, oldY, newY) -> { + if (!isDetached()) { + setAnchorY(getAnchorY() + (newY.doubleValue() - oldY.doubleValue())); } }; - private WeakChangeListener weakYListener = new WeakChangeListener<>( + private final WeakChangeListener weakYListener = new WeakChangeListener<>( yListener); private Window ownerWindow; + private final EventHandler closePopOverOnOwnerWindowCloseLambda = event -> ownerWindowClosing(); + private final WeakEventHandler closePopOverOnOwnerWindowClose = new WeakEventHandler<>(closePopOverOnOwnerWindowCloseLambda); /** * Shows the pop over in a position relative to the edges of the given owner @@ -286,6 +332,38 @@ public class PopOver extends PopupControl { } } + /** {@inheritDoc} */ + @Override + public final void show(Window owner) { + super.show(owner); + ownerWindow = owner; + + if (isAnimated()) { + showFadeInAnimation(getFadeInDuration()); + } + + ownerWindow.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, + closePopOverOnOwnerWindowClose); + ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING, + closePopOverOnOwnerWindowClose); + } + + /** {@inheritDoc} */ + @Override + public final void show(Window ownerWindow, double anchorX, double anchorY) { + super.show(ownerWindow, anchorX, anchorY); + this.ownerWindow = ownerWindow; + + if (isAnimated()) { + showFadeInAnimation(getFadeInDuration()); + } + + ownerWindow.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, + closePopOverOnOwnerWindowClose); + ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING, + closePopOverOnOwnerWindowClose); + } + /** * Makes the pop over visible at the give location and associates it with * the given owner node. The x and y coordinate will be the target location @@ -300,7 +378,7 @@ public class PopOver extends PopupControl { */ @Override public final void show(Node owner, double x, double y) { - show(owner, x, y, DEFAULT_FADE_DURATION); + show(owner, x, y, getFadeInDuration()); } /** @@ -315,14 +393,14 @@ public class PopOver extends PopupControl { * @param y * the y coordinate for the pop over arrow tip * @param fadeInDuration - * the time it takes for the pop over to be fully visible + * the time it takes for the pop over to be fully visible. This duration takes precedence over the fade-in property without setting. */ public final void show(Node owner, double x, double y, Duration fadeInDuration) { /* - * Calling show() a second time without first closing the - * pop over causes it to be placed at the wrong location. + * Calling show() a second time without first closing the pop over + * causes it to be placed at the wrong location. */ if (ownerWindow != null && isShowing()) { super.hide(); @@ -360,18 +438,15 @@ public class PopOver extends PopupControl { /* * The user clicked somewhere into the transparent background. If - * this is the case the hide the window (when attached). + * this is the case then hide the window (when attached). */ - getScene().addEventHandler(MOUSE_CLICKED, - new EventHandler() { - public void handle(MouseEvent evt) { - if (evt.getTarget().equals(getScene().getRoot())) { - if (!isDetached()) { - hide(); - } - } - }; - }); + getScene().addEventHandler(MOUSE_CLICKED, mouseEvent -> { + if (mouseEvent.getTarget().equals(getScene().getRoot())) { + if (!isDetached()) { + hide(); + } + } + }); /* * Move the window so that the arrow will end up pointing at the @@ -382,6 +457,18 @@ public class PopOver extends PopupControl { super.show(owner, x, y); + if (isAnimated()) { + showFadeInAnimation(fadeInDuration); + } + + // Bug fix - close popup when owner window is closing + ownerWindow.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, + closePopOverOnOwnerWindowClose); + ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING, + closePopOverOnOwnerWindowClose); + } + + private void showFadeInAnimation(Duration fadeInDuration) { // Fade In Node skinNode = getSkin().getNode(); skinNode.setOpacity(0); @@ -392,6 +479,10 @@ public class PopOver extends PopupControl { fadeIn.play(); } + private void ownerWindowClosing() { + hide(Duration.ZERO); + } + /** * Hides the pop over by quickly changing its opacity to 0. * @@ -399,7 +490,7 @@ public class PopOver extends PopupControl { */ @Override public final void hide() { - hide(DEFAULT_FADE_DURATION); + hide(getFadeOutDuration()); } /** @@ -411,21 +502,32 @@ public class PopOver extends PopupControl { * @since 1.0 */ public final void hide(Duration fadeOutDuration) { + log.info("hide:" + fadeOutDuration.toString()); + //We must remove EventFilter in order to prevent memory leak. + if (ownerWindow != null) { + ownerWindow.removeEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, + closePopOverOnOwnerWindowClose); + ownerWindow.removeEventFilter(WindowEvent.WINDOW_HIDING, + closePopOverOnOwnerWindowClose); + } if (fadeOutDuration == null) { fadeOutDuration = DEFAULT_FADE_DURATION; } if (isShowing()) { - // Fade Out - Node skinNode = getSkin().getNode(); - skinNode.setOpacity(0); + if (isAnimated()) { + // Fade Out + Node skinNode = getSkin().getNode(); - FadeTransition fadeOut = new FadeTransition(fadeOutDuration, - skinNode); - fadeOut.setFromValue(1); - fadeOut.setToValue(0); - fadeOut.setOnFinished(evt -> super.hide()); - fadeOut.play(); + FadeTransition fadeOut = new FadeTransition(fadeOutDuration, + skinNode); + fadeOut.setFromValue(skinNode.getOpacity()); + fadeOut.setToValue(0); + fadeOut.setOnFinished(evt -> super.hide()); + fadeOut.play(); + } else { + super.hide(); + } } } @@ -436,26 +538,26 @@ public class PopOver extends PopupControl { case TOP_CENTER: case TOP_LEFT: case TOP_RIGHT: - setX(getX() + bounds.getMinX() - computeXOffset()); - setY(getY() + bounds.getMinY() + getArrowSize()); + setAnchorX(getAnchorX() + bounds.getMinX() - computeXOffset()); + setAnchorY(getAnchorY() + bounds.getMinY() + getArrowSize()); break; case LEFT_TOP: case LEFT_CENTER: case LEFT_BOTTOM: - setX(getX() + bounds.getMinX() + getArrowSize()); - setY(getY() + bounds.getMinY() - computeYOffset()); + setAnchorX(getAnchorX() + bounds.getMinX() + getArrowSize()); + setAnchorY(getAnchorY() + bounds.getMinY() - computeYOffset()); break; case BOTTOM_CENTER: case BOTTOM_LEFT: case BOTTOM_RIGHT: - setX(getX() + bounds.getMinX() - computeXOffset()); - setY(getY() - bounds.getMinY() - bounds.getMaxY() - 1); + setAnchorX(getAnchorX() + bounds.getMinX() - computeXOffset()); + setAnchorY(getAnchorY() - bounds.getMinY() - bounds.getMaxY() - 1); break; case RIGHT_TOP: case RIGHT_BOTTOM: case RIGHT_CENTER: - setX(getX() - bounds.getMinX() - bounds.getMaxX() - 1); - setY(getY() + bounds.getMinY() - computeYOffset()); + setAnchorX(getAnchorX() - bounds.getMinX() - bounds.getMaxX() - 1); + setAnchorY(getAnchorY() + bounds.getMinY() - computeYOffset()); break; } } @@ -508,6 +610,74 @@ public class PopOver extends PopupControl { } } + // always show header + + private final BooleanProperty headerAlwaysVisible = new SimpleBooleanProperty(this, "headerAlwaysVisible"); //$NON-NLS-1$ + + /** + * Determines whether or not the {@link PopOver} header should remain visible, even while attached. + */ + public final BooleanProperty headerAlwaysVisibleProperty() { + return headerAlwaysVisible; + } + + /** + * Sets the value of the headerAlwaysVisible property. + * + * @param visible + * if true, then the header is visible even while attached + * + * @see #headerAlwaysVisibleProperty() + */ + public final void setHeaderAlwaysVisible(boolean visible) { + headerAlwaysVisible.setValue(visible); + } + + /** + * Returns the value of the detachable property. + * + * @return true if the header is visible even while attached + * + * @see #headerAlwaysVisibleProperty() + */ + public final boolean isHeaderAlwaysVisible() { + return headerAlwaysVisible.getValue(); + } + + // enable close button + + private final BooleanProperty closeButtonEnabled = new SimpleBooleanProperty(this, "closeButtonEnabled", true); //$NON-NLS-1$ + + /** + * Determines whether or not the header's close button should be available. + */ + public final BooleanProperty closeButtonEnabledProperty() { + return closeButtonEnabled; + } + + /** + * Sets the value of the closeButtonEnabled property. + * + * @param enabled + * if false, the pop over will not be closeable by the header's close button + * + * @see #closeButtonEnabledProperty() + */ + public final void setCloseButtonEnabled(boolean enabled) { + closeButtonEnabled.setValue(enabled); + } + + /** + * Returns the value of the closeButtonEnabled property. + * + * @return true if the header's close button is enabled + * + * @see #closeButtonEnabledProperty() + */ + public final boolean isCloseButtonEnabled() { + return closeButtonEnabled.getValue(); + } + // detach support private final BooleanProperty detachable = new SimpleBooleanProperty(this, @@ -561,7 +731,7 @@ public class PopOver extends PopupControl { * Sets the value of the detached property. * * @param detached - * if true the pop over will change its appearance to "detached" + * if true the pop over will change its apperance to "detached" * mode * * @see #detachedProperty() @@ -701,46 +871,42 @@ public class PopOver extends PopupControl { // Detached stage title - private final StringProperty detachedTitle = new SimpleStringProperty(this, - "detachedTitle", "Info"); //$NON-NLS-1$ //$NON-NLS-2$ + private final StringProperty title = new SimpleStringProperty(this, "title", "No title set"); //$NON-NLS-1$ //$NON-NLS-2$ /** - * Stores the title to display when the pop over becomes detached. + * Stores the title to display in the PopOver's header. * - * @return the detached title property + * @return the title property */ - public final StringProperty detachedTitleProperty() { - return detachedTitle; + public final StringProperty titleProperty() { + return title; } /** - * Returns the value of the detached title property. + * Returns the value of the title property. * * @return the detached title - * - * @see #detachedTitleProperty() + * @see #titleProperty() */ - public final String getDetachedTitle() { - return detachedTitleProperty().get(); + public final String getTitle() { + return titleProperty().get(); } /** - * Sets the value of the detached title property. + * Sets the value of the title property. * - * @param title - * the title to use when detached - * - * @see #detachedTitleProperty() + * @param title the title to use when detached + * @see #titleProperty() */ - public final void setDetachedTitle(String title) { + public final void setTitle(String title) { if (title == null) { throw new IllegalArgumentException("title can not be null"); //$NON-NLS-1$ } - detachedTitleProperty().set(title); + titleProperty().set(title); } - private final ObjectProperty arrowLocation = new SimpleObjectProperty( + private final ObjectProperty arrowLocation = new SimpleObjectProperty<>( this, "arrowLocation", ArrowLocation.LEFT_TOP); //$NON-NLS-1$ /** @@ -782,6 +948,93 @@ public class PopOver extends PopupControl { * All possible arrow locations. */ public enum ArrowLocation { - LEFT_TOP, LEFT_CENTER, LEFT_BOTTOM, RIGHT_TOP, RIGHT_CENTER, RIGHT_BOTTOM, TOP_LEFT, TOP_CENTER, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT; + LEFT_TOP, LEFT_CENTER, LEFT_BOTTOM, RIGHT_TOP, RIGHT_CENTER, RIGHT_BOTTOM, TOP_LEFT, TOP_CENTER, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT + } + + /** + * Stores the fade-in duration. This should be set before calling PopOver.show(..). + * + * @return the fade-in duration property + */ + public final ObjectProperty fadeInDurationProperty() { + return fadeInDuration; + } + + /** + * Stores the fade-out duration. + * + * @return the fade-out duration property + */ + public final ObjectProperty fadeOutDurationProperty() { + return fadeOutDuration; + } + + /** + * Returns the value of the fade-in duration property. + * + * @return the fade-in duration + * @see #fadeInDurationProperty() + */ + public final Duration getFadeInDuration() { + return fadeInDurationProperty().get(); + } + + /** + * Sets the value of the fade-in duration property. This should be set before calling PopOver.show(..). + * + * @param duration the requested fade-in duration + * @see #fadeInDurationProperty() + */ + public final void setFadeInDuration(Duration duration) { + fadeInDurationProperty().setValue(duration); + } + + /** + * Returns the value of the fade-out duration property. + * + * @return the fade-out duration + * @see #fadeOutDurationProperty() + */ + public final Duration getFadeOutDuration() { + return fadeOutDurationProperty().get(); + } + + /** + * Sets the value of the fade-out duration property. + * + * @param duration the requested fade-out duration + * @see #fadeOutDurationProperty() + */ + public final void setFadeOutDuration(Duration duration) { + fadeOutDurationProperty().setValue(duration); + } + + /** + * Stores the "animated" flag. If true then the PopOver will be shown / hidden with a short fade in / out animation. + * + * @return the "animated" property + */ + public final BooleanProperty animatedProperty() { + return animated; + } + + /** + * Returns the value of the "animated" property. + * + * @return true if the PopOver will be shown and hidden with a short fade animation + * @see #animatedProperty() + */ + public final boolean isAnimated() { + return animatedProperty().get(); + } + + /** + * Sets the value of the "animated" property. + * + * @param animated if true the PopOver will be shown and hidden with a short fade animation + * @see #animatedProperty() + */ + public final void setAnimated(boolean animated) { + animatedProperty().set(animated); } } diff --git a/desktop/src/main/java/bisq/desktop/components/controlsfx/control/popover.css b/desktop/src/main/java/bisq/desktop/components/controlsfx/control/popover.css index ccc6d5e42f..79f946fe6e 100644 --- a/desktop/src/main/java/bisq/desktop/components/controlsfx/control/popover.css +++ b/desktop/src/main/java/bisq/desktop/components/controlsfx/control/popover.css @@ -3,10 +3,10 @@ } .popover > .border { - -fx-stroke: linear-gradient(to bottom, rgba(0,0,0, .3), rgba(0, 0, 0, .7)) ; - -fx-stroke-width: 0.5; - -fx-fill: rgba(255.0,255.0,255.0, .95); - -fx-effect: dropshadow(gaussian, rgba(0,0,0,.2), 10.0, 0.5, 2.0, 2.0); + -fx-stroke: linear-gradient(to bottom, rgba(0, 0, 0, .3), rgba(0, 0, 0, .7)); + -fx-stroke-width: 1; + -fx-fill: rgba(255.0, 255.0, 255.0, .95); + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, .2), 10.0, 0.5, 2.0, 2.0); } .popover > .content { @@ -15,7 +15,7 @@ .popover > .detached { } -.popover > .content > .title > .text { +.popover > .content > .title > .text { -fx-padding: 6.0 6.0 0.0 6.0; -fx-text-fill: rgba(120, 120, 120, .8); -fx-font-weight: bold; @@ -26,11 +26,11 @@ } .popover > .content > .title > .icon > .graphics > .circle { - -fx-fill: gray ; - -fx-effect: innershadow(gaussian, rgba(0,0,0,.2), 3, 0.5, 1.0, 1.0); + -fx-fill: gray; + -fx-effect: innershadow(gaussian, rgba(0, 0, 0, .2), 3, 0.5, 1.0, 1.0); } .popover > .content > .title > .icon > .graphics > .line { - -fx-stroke: white ; + -fx-stroke: white; -fx-stroke-width: 2; } diff --git a/desktop/src/main/java/bisq/desktop/components/controlsfx/skin/PopOverSkin.java b/desktop/src/main/java/bisq/desktop/components/controlsfx/skin/PopOverSkin.java index 3ebdc843bf..e449f847a1 100644 --- a/desktop/src/main/java/bisq/desktop/components/controlsfx/skin/PopOverSkin.java +++ b/desktop/src/main/java/bisq/desktop/components/controlsfx/skin/PopOverSkin.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2013 - 2015, ControlsFX * All rights reserved. * @@ -26,24 +26,11 @@ */ package bisq.desktop.components.controlsfx.skin; -import static java.lang.Double.MAX_VALUE; -import static javafx.geometry.Pos.CENTER_LEFT; -import static javafx.scene.control.ContentDisplay.GRAPHIC_ONLY; -import static bisq.desktop.components.controlsfx.control.PopOver.ArrowLocation.*; +import bisq.desktop.components.controlsfx.control.PopOver; +import bisq.desktop.components.controlsfx.control.PopOver.ArrowLocation; -import java.util.ArrayList; -import java.util.List; +import javafx.stage.Window; -import javafx.beans.InvalidationListener; -import javafx.beans.Observable; -import javafx.beans.binding.Bindings; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.SimpleDoubleProperty; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; -import javafx.event.EventHandler; -import javafx.geometry.Point2D; -import javafx.geometry.Pos; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.control.Label; @@ -60,10 +47,28 @@ import javafx.scene.shape.Path; import javafx.scene.shape.PathElement; import javafx.scene.shape.QuadCurveTo; import javafx.scene.shape.VLineTo; -import javafx.stage.Window; -import bisq.desktop.components.controlsfx.control.PopOver; -import bisq.desktop.components.controlsfx.control.PopOver.ArrowLocation; +import javafx.geometry.Point2D; +import javafx.geometry.Pos; + +import javafx.beans.InvalidationListener; +import javafx.beans.binding.Bindings; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; + +import javafx.event.EventHandler; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static bisq.desktop.components.controlsfx.control.PopOver.ArrowLocation.*; +import static java.lang.Double.MAX_VALUE; +import static javafx.geometry.Pos.CENTER_LEFT; +import static javafx.scene.control.ContentDisplay.GRAPHIC_ONLY; +import static javafx.scene.paint.Color.YELLOW; public class PopOverSkin implements Skin { @@ -74,27 +79,27 @@ public class PopOverSkin implements Skin { private boolean tornOff; - private Label title; - private Label closeIcon; + private final Path path; + private final Path clip; - private Path path; - private BorderPane content; - private StackPane titlePane; - private StackPane stackPane; + private final BorderPane content; + private final StackPane titlePane; + private final StackPane stackPane; private Point2D dragStartLocation; - private PopOver popOver; + private final PopOver popOver; + + private final Logger log = LoggerFactory.getLogger(this.getClass()); public PopOverSkin(final PopOver popOver) { this.popOver = popOver; - stackPane = new StackPane(); - stackPane.getStylesheets().add( - PopOver.class.getResource("popover.css").toExternalForm()); //$NON-NLS-1$ + stackPane = popOver.getRoot(); stackPane.setPickOnBounds(false); - stackPane.getStyleClass().add("popover"); //$NON-NLS-1$ + + Bindings.bindContent(stackPane.getStyleClass(), popOver.getStyleClass()); /* * The min width and height equal 2 * corner radius + 2 * arrow indent + @@ -110,26 +115,22 @@ public class PopOverSkin implements Skin { stackPane.minHeightProperty().bind(stackPane.minWidthProperty()); - title = new Label(); - title.textProperty().bind(popOver.detachedTitleProperty()); + Label title = new Label(); + title.textProperty().bind(popOver.titleProperty()); title.setMaxSize(MAX_VALUE, MAX_VALUE); title.setAlignment(Pos.CENTER); title.getStyleClass().add("text"); //$NON-NLS-1$ - closeIcon = new Label(); + Label closeIcon = new Label(); closeIcon.setGraphic(createCloseIcon()); closeIcon.setMaxSize(MAX_VALUE, MAX_VALUE); closeIcon.setContentDisplay(GRAPHIC_ONLY); - closeIcon.visibleProperty().bind(popOver.detachedProperty()); + closeIcon.visibleProperty().bind( + popOver.closeButtonEnabledProperty().and( + popOver.detachedProperty().or(popOver.headerAlwaysVisibleProperty()))); closeIcon.getStyleClass().add("icon"); //$NON-NLS-1$ closeIcon.setAlignment(CENTER_LEFT); - closeIcon.getGraphic().setOnMouseClicked( - new EventHandler() { - @Override - public void handle(MouseEvent evt) { - popOver.hide(); - } - }); + closeIcon.getGraphic().setOnMouseClicked(evt -> popOver.hide()); titlePane = new StackPane(); titlePane.getChildren().add(title); @@ -140,105 +141,125 @@ public class PopOverSkin implements Skin { content.setCenter(popOver.getContentNode()); content.getStyleClass().add("content"); //$NON-NLS-1$ - if (popOver.isDetached()) { + if (popOver.isDetached() || popOver.isHeaderAlwaysVisible()) { content.setTop(titlePane); + } + + if (popOver.isDetached()) { popOver.getStyleClass().add(DETACHED_STYLE_CLASS); content.getStyleClass().add(DETACHED_STYLE_CLASS); } - InvalidationListener updatePathListener = new InvalidationListener() { - - @Override - public void invalidated(Observable observable) { - updatePath(); + popOver.headerAlwaysVisibleProperty().addListener((o, oV, isVisible) -> { + if (isVisible) { + content.setTop(titlePane); + } else if (!popOver.isDetached()) { + content.setTop(null); } - }; + }); + InvalidationListener updatePathListener = observable -> updatePath(); getPopupWindow().xProperty().addListener(updatePathListener); getPopupWindow().yProperty().addListener(updatePathListener); - popOver.arrowLocationProperty().addListener(updatePathListener); + popOver.contentNodeProperty().addListener( + (value, oldContent, newContent) -> content + .setCenter(newContent)); + popOver.detachedProperty() + .addListener((value, oldDetached, newDetached) -> { - popOver.contentNodeProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue value, - Node oldContent, Node newContent) { - content.setCenter(newContent); - } - }); + if (newDetached) { + popOver.getStyleClass().add(DETACHED_STYLE_CLASS); + content.getStyleClass().add(DETACHED_STYLE_CLASS); + content.setTop(titlePane); - popOver.detachedProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue value, - Boolean oldDetached, Boolean newDetached) { + switch (getSkinnable().getArrowLocation()) { + case LEFT_TOP: + case LEFT_CENTER: + case LEFT_BOTTOM: + popOver.setAnchorX( + popOver.getAnchorX() + popOver.getArrowSize()); + break; + case TOP_LEFT: + case TOP_CENTER: + case TOP_RIGHT: + popOver.setAnchorY( + popOver.getAnchorY() + popOver.getArrowSize()); + break; + default: + break; + } + } else { + popOver.getStyleClass().remove(DETACHED_STYLE_CLASS); + content.getStyleClass().remove(DETACHED_STYLE_CLASS); - updatePath(); + if (!popOver.isHeaderAlwaysVisible()) { + content.setTop(null); + } + } - if (newDetached) { - popOver.getStyleClass().add(DETACHED_STYLE_CLASS); - content.getStyleClass().add(DETACHED_STYLE_CLASS); - content.setTop(titlePane); - } else { - popOver.getStyleClass().remove(DETACHED_STYLE_CLASS); - content.getStyleClass().remove(DETACHED_STYLE_CLASS); - content.setTop(null); - } - } - }); + popOver.sizeToScene(); + + updatePath(); + }); path = new Path(); path.getStyleClass().add("border"); //$NON-NLS-1$ path.setManaged(false); + clip = new Path(); + + /* + * The clip is a path and the path has to be filled with a color. + * Otherwise clipping will not work. + */ + clip.setFill(YELLOW); + createPathElements(); updatePath(); - final EventHandler mousePressedHandler = new EventHandler() { - public void handle(MouseEvent evt) { - if (popOver.isDetachable() || popOver.isDetached()) { - tornOff = false; + final EventHandler mousePressedHandler = evt -> { + log.info("mousePressed:" + popOver.isDetachable() + "," + popOver.isDetached()); + if (popOver.isDetachable() || popOver.isDetached()) { + tornOff = false; - xOffset = evt.getScreenX(); - yOffset = evt.getScreenY(); + xOffset = evt.getScreenX(); + yOffset = evt.getScreenY(); - dragStartLocation = new Point2D(xOffset, yOffset); - } - }; + dragStartLocation = new Point2D(xOffset, yOffset); + } }; - final EventHandler mouseReleasedHandler = new EventHandler() { - public void handle(MouseEvent evt) { - if (tornOff && !getSkinnable().isDetached()) { - tornOff = false; - getSkinnable().detach(); - } - }; + final EventHandler mouseReleasedHandler = evt -> { + log.info("mouseReleased:tornOff" + tornOff + ", " + !getSkinnable().isDetached()); + if (tornOff && !getSkinnable().isDetached()) { + tornOff = false; + getSkinnable().detach(); + } }; - final EventHandler mouseDragHandler = new EventHandler() { + final EventHandler mouseDragHandler = evt -> { + log.info("mouseDrag:" + popOver.isDetachable() + "," + popOver.isDetached()); + if (popOver.isDetachable() || popOver.isDetached()) { + double deltaX = evt.getScreenX() - xOffset; + double deltaY = evt.getScreenY() - yOffset; - public void handle(MouseEvent evt) { - if (popOver.isDetachable() || popOver.isDetached()) { - double deltaX = evt.getScreenX() - xOffset; - double deltaY = evt.getScreenY() - yOffset; + Window window = getSkinnable().getScene().getWindow(); - Window window = getSkinnable().getScene().getWindow(); + window.setX(window.getX() + deltaX); + window.setY(window.getY() + deltaY); - window.setX(window.getX() + deltaX); - window.setY(window.getY() + deltaY); + xOffset = evt.getScreenX(); + yOffset = evt.getScreenY(); - xOffset = evt.getScreenX(); - yOffset = evt.getScreenY(); - - if (dragStartLocation.distance(xOffset, yOffset) > 20) { - tornOff = true; - updatePath(); - } else if (tornOff) { - tornOff = false; - updatePath(); - } + if (dragStartLocation.distance(xOffset, yOffset) > 20) { + tornOff = true; + updatePath(); + } else if (tornOff) { + tornOff = false; + updatePath(); } - }; + } }; stackPane.setOnMousePressed(mousePressedHandler); @@ -247,6 +268,8 @@ public class PopOverSkin implements Skin { stackPane.getChildren().add(path); stackPane.getChildren().add(content); + + content.setClip(clip); } @Override @@ -689,5 +712,6 @@ public class PopOverSkin implements Skin { elements.add(topCurveTo); path.getElements().setAll(elements); + clip.getElements().setAll(elements); } } diff --git a/desktop/src/main/java/bisq/desktop/components/list/FilterBox.java b/desktop/src/main/java/bisq/desktop/components/list/FilterBox.java new file mode 100644 index 0000000000..8de9bc4d56 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/list/FilterBox.java @@ -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 . + */ + +package bisq.desktop.components.list; + +import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.filtering.FilterableListItem; + +import bisq.core.locale.Res; + +import javafx.scene.control.TableView; +import javafx.scene.layout.HBox; + +import javafx.geometry.Insets; +import javafx.beans.value.ChangeListener; +import javafx.collections.transformation.FilteredList; + +public class FilterBox extends HBox { + private final InputTextField textField; + private FilteredList filteredList; + + private ChangeListener listener; + + public FilterBox() { + super(); + setSpacing(5.0); + + AutoTooltipLabel label = new AutoTooltipLabel(Res.get("shared.filter")); + HBox.setMargin(label, new Insets(5.0, 0, 0, 10.0)); + + textField = new InputTextField(); + textField.setMinWidth(500); + + getChildren().addAll(label, textField); + } + + public void initialize(FilteredList filteredList, + TableView tableView) { + this.filteredList = filteredList; + listener = (observable, oldValue, newValue) -> { + tableView.getSelectionModel().clearSelection(); + applyFilteredListPredicate(textField.getText()); + }; + } + + public void activate() { + textField.textProperty().addListener(listener); + applyFilteredListPredicate(textField.getText()); + } + + public void deactivate() { + textField.textProperty().removeListener(listener); + } + + private void applyFilteredListPredicate(String filterString) { + filteredList.setPredicate(item -> item.match(filterString)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AchTransferForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AchTransferForm.java new file mode 100644 index 0000000000..e4bd32f5db --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AchTransferForm.java @@ -0,0 +1,102 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.BankUtil; +import bisq.core.locale.Country; +import bisq.core.locale.Res; +import bisq.core.payment.AchTransferAccount; +import bisq.core.payment.CountryBasedPaymentAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.AchTransferAccountPayload; +import bisq.core.payment.payload.BankAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.ComboBox; +import javafx.scene.layout.GridPane; + +import javafx.collections.FXCollections; + +import static bisq.desktop.util.FormBuilder.*; + +public class AchTransferForm extends GeneralUsBankForm { + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + AchTransferAccountPayload achTransferAccountPayload = (AchTransferAccountPayload) paymentAccountPayload; + return addFormForBuyer(gridPane, gridRow, paymentAccountPayload, achTransferAccountPayload.getAccountType(), achTransferAccountPayload.getHolderAddress()); + } + + private final AchTransferAccount achTransferAccount; + + public AchTransferForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.achTransferAccount = (AchTransferAccount) paymentAccount; + } + + @Override + public void addFormForEditAccount() { + addFormForEditAccount(achTransferAccount.getPayload(), achTransferAccount.getPayload().getHolderAddress()); + } + + @Override + public void addFormForAddAccount() { + addFormForAddAccountInternal(achTransferAccount.getPayload(), achTransferAccount.getPayload().getHolderAddress()); + } + + @Override + protected void setHolderAddress(String holderAddress) { + achTransferAccount.getPayload().setHolderAddress(holderAddress); + } + + @Override + protected void maybeAddAccountTypeCombo(BankAccountPayload bankAccountPayload, Country country) { + ComboBox accountTypeComboBox = addComboBox(gridPane, ++gridRow, Res.get("payment.select.account")); + accountTypeComboBox.setItems(FXCollections.observableArrayList(BankUtil.getAccountTypeValues(country.code))); + accountTypeComboBox.setOnAction(e -> { + if (BankUtil.isAccountTypeRequired(country.code)) { + bankAccountPayload.setAccountType(accountTypeComboBox.getSelectionModel().getSelectedItem()); + updateFromInputs(); + } + }); + } + + @Override + public void updateAllInputsValid() { + AchTransferAccountPayload achTransferAccountPayload = achTransferAccount.getPayload(); + boolean result = isAccountNameValid() + && paymentAccount.getSingleTradeCurrency() != null + && ((CountryBasedPaymentAccount) this.paymentAccount).getCountry() != null + && inputValidator.validate(achTransferAccountPayload.getHolderName()).isValid + && inputValidator.validate(achTransferAccountPayload.getHolderAddress()).isValid; + + result = getValidationResult(result, + achTransferAccountPayload.getCountryCode(), + achTransferAccountPayload.getBankName(), + achTransferAccountPayload.getBankId(), + achTransferAccountPayload.getBranchId(), + achTransferAccountPayload.getAccountNr(), + achTransferAccountPayload.getAccountType(), + achTransferAccountPayload.getHolderTaxId(), + achTransferAccountPayload.getNationalAccountId()); + allInputsValid.set(result); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AdvancedCashForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AdvancedCashForm.java index e6f768e6ee..6937ab1614 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AdvancedCashForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AdvancedCashForm.java @@ -19,11 +19,9 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.AdvancedCashValidator; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.payment.AdvancedCashAccount; import bisq.core.payment.PaymentAccount; @@ -34,26 +32,18 @@ import bisq.core.util.validation.InputValidator; import bisq.common.util.Tuple2; -import org.apache.commons.lang3.StringUtils; - import javafx.scene.control.Label; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static bisq.desktop.util.FormBuilder.addTopLabelFlowPane; @Deprecated public class AdvancedCashForm extends PaymentMethodForm { - private static final Logger log = LoggerFactory.getLogger(AdvancedCashForm.class); - private final AdvancedCashAccount advancedCashAccount; private final AdvancedCashValidator advancedCashValidator; - private InputTextField accountNrInputTextField; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { @@ -62,8 +52,13 @@ public class AdvancedCashForm extends PaymentMethodForm { return gridRow; } - public AdvancedCashForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, AdvancedCashValidator advancedCashValidator, - InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + public AdvancedCashForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, + AdvancedCashValidator advancedCashValidator, + InputValidator inputValidator, + GridPane gridPane, + int gridRow, + CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.advancedCashAccount = (AdvancedCashAccount) paymentAccount; this.advancedCashValidator = advancedCashValidator; @@ -73,7 +68,7 @@ public class AdvancedCashForm extends PaymentMethodForm { public void addFormForAddAccount() { gridRowFrom = gridRow + 1; - accountNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.wallet")); + InputTextField accountNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.wallet")); accountNrInputTextField.setValidator(advancedCashValidator); accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { advancedCashAccount.setAccountNr(newValue); @@ -95,25 +90,19 @@ public class AdvancedCashForm extends PaymentMethodForm { else flowPane.setId("flow-pane-checkboxes-non-editable-bg"); - CurrencyUtil.getAllAdvancedCashCurrencies().stream().forEach(e -> + paymentAccount.getSupportedCurrencies().forEach(e -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, advancedCashAccount)); } @Override protected void autoFillNameTextField() { - if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { - String accountNr = accountNrInputTextField.getText(); - accountNr = StringUtils.abbreviate(accountNr, 9); - String method = Res.get(paymentAccount.getPaymentMethod().getId()); - accountNameTextField.setText(method.concat(": ").concat(accountNr)); - } + setAccountNameWithString(advancedCashAccount.getAccountNr()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addCompactTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - advancedCashAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(advancedCashAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.wallet"), diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AmazonGiftCardForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AmazonGiftCardForm.java index 41c1ad4798..51a153f676 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AmazonGiftCardForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AmazonGiftCardForm.java @@ -30,7 +30,6 @@ import bisq.core.payment.AmazonGiftCardAccount; import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.AmazonGiftCardAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; -import bisq.core.payment.payload.PaymentMethod; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.InputValidator; @@ -50,7 +49,6 @@ import static bisq.desktop.util.FormBuilder.*; @Slf4j public class AmazonGiftCardForm extends PaymentMethodForm { - private InputTextField accountNrInputTextField; ComboBox countryCombo; private final AmazonGiftCardAccount amazonGiftCardAccount; @@ -87,7 +85,7 @@ public class AmazonGiftCardForm extends PaymentMethodForm { public void addFormForAddAccount() { gridRowFrom = gridRow + 1; - accountNrInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.email.mobile")); + InputTextField accountNrInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.email.mobile")); accountNrInputTextField.setValidator(inputValidator); accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { amazonGiftCardAccount.setEmailOrMobileNr(newValue); @@ -123,14 +121,25 @@ public class AmazonGiftCardForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(accountNrInputTextField.getText()); + setAccountNameWithString(amazonGiftCardAccount.getEmailOrMobileNr()); } @Override - public void addFormForDisplayAccount() { - addFormForAccountNumberDisplayAccount(paymentAccount.getAccountName(), paymentAccount.getPaymentMethod(), - amazonGiftCardAccount.getEmailOrMobileNr(), - paymentAccount.getSingleTradeCurrency()); + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(paymentAccount.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, + Res.get("payment.email.mobile"), amazonGiftCardAccount.getEmailOrMobileNr()).second; + field.setMouseTransparent(false); + + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), + amazonGiftCardAccount.getCountry() != null ? amazonGiftCardAccount.getCountry().name : ""); + String nameAndCode = paymentAccount.getSingleTradeCurrency() != null ? paymentAccount.getSingleTradeCurrency().getNameAndCode() : ""; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + + addLimitations(true); } @Override @@ -140,27 +149,6 @@ public class AmazonGiftCardForm extends PaymentMethodForm { && paymentAccount.getTradeCurrencies().size() > 0); } - private void addFormForAccountNumberDisplayAccount(String accountName, - PaymentMethod paymentMethod, - String accountNr, - TradeCurrency singleTradeCurrency) { - gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), accountName, - Layout.FIRST_ROW_AND_GROUP_DISTANCE); - addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), - Res.get(paymentMethod.getId())); - TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, - Res.get("payment.email.mobile"), accountNr).second; - field.setMouseTransparent(false); - - addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), - amazonGiftCardAccount.getCountry() != null ? amazonGiftCardAccount.getCountry().name : ""); - String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; - addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); - - addLimitations(true); - } - private static String countryToAmazonSite(String countryCode) { HashMap mapCountryToSite = new HashMap<>() {{ put("AU", "https://www.amazon.au"); diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java index 22b435ff38..cf8cfdf20d 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java @@ -40,8 +40,6 @@ import bisq.core.util.validation.InputValidator; import bisq.common.UserThread; import bisq.common.util.Tuple3; -import org.apache.commons.lang3.StringUtils; - import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.TextField; @@ -52,10 +50,10 @@ import javafx.geometry.Insets; import javafx.util.StringConverter; +import static bisq.desktop.util.DisplayUtils.createAssetsAccountName; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static bisq.desktop.util.FormBuilder.addLabelCheckBox; -import static bisq.desktop.util.FormBuilder.addTopLabelTextField; import static bisq.desktop.util.GUIUtil.getComboBoxButtonCell; public class AssetsForm extends PaymentMethodForm { @@ -163,20 +161,14 @@ public class AssetsForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { - String currency = paymentAccount.getSingleTradeCurrency() != null ? paymentAccount.getSingleTradeCurrency().getCode() : ""; - if (currency != null) { - String address = addressInputTextField.getText(); - address = StringUtils.abbreviate(address, 9); - accountNameTextField.setText(currency.concat(": ").concat(address)); - } + accountNameTextField.setText(createAssetsAccountName(paymentAccount, assetAccount.getAddress())); } } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - assetAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(assetAccount.getPaymentMethod().getId())); Tuple3 tuple2 = addCompactTopLabelTextField(gridPane, ++gridRow, @@ -232,8 +224,13 @@ public class AssetsForm extends PaymentMethodForm { ((AutocompleteComboBox) currencyComboBox).setOnChangeConfirmed(e -> { addressInputTextField.resetValidation(); addressInputTextField.validate(); - paymentAccount.setSingleTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem()); + TradeCurrency tradeCurrency = currencyComboBox.getSelectionModel().getSelectedItem(); + paymentAccount.setSingleTradeCurrency(tradeCurrency); updateFromInputs(); + + if (tradeCurrency != null && tradeCurrency.getCode().equals("BSQ")) { + new Popup().information(Res.get("payment.select.altcoin.bsq.warning")).show(); + } }); } } diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AustraliaPayidForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AustraliaPayidForm.java index 14d253b609..1101a9725b 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AustraliaPayidForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AustraliaPayidForm.java @@ -19,15 +19,14 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.AustraliaPayidValidator; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; -import bisq.core.payment.AustraliaPayid; +import bisq.core.payment.AustraliaPayidAccount; import bisq.core.payment.PaymentAccount; -import bisq.core.payment.payload.AustraliaPayidPayload; +import bisq.core.payment.payload.AustraliaPayidAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.InputValidator; @@ -39,15 +38,14 @@ import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addTopLabelTextField; public class AustraliaPayidForm extends PaymentMethodForm { - private final AustraliaPayid australiaPayid; + private final AustraliaPayidAccount australiaPayidAccount; private final AustraliaPayidValidator australiaPayidValidator; - private InputTextField mobileNrInputTextField; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), - ((AustraliaPayidPayload) paymentAccountPayload).getBankAccountName()); + ((AustraliaPayidAccountPayload) paymentAccountPayload).getBankAccountName()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.payid"), - ((AustraliaPayidPayload) paymentAccountPayload).getPayid()); + ((AustraliaPayidAccountPayload) paymentAccountPayload).getPayid()); return gridRow; } @@ -59,7 +57,7 @@ public class AustraliaPayidForm extends PaymentMethodForm { int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); - this.australiaPayid = (AustraliaPayid) paymentAccount; + this.australiaPayidAccount = (AustraliaPayidAccount) paymentAccount; this.australiaPayidValidator = australiaPayidValidator; } @@ -71,18 +69,18 @@ public class AustraliaPayidForm extends PaymentMethodForm { Res.get("payment.account.owner")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { - australiaPayid.setBankAccountName(newValue); + australiaPayidAccount.setBankAccountName(newValue); updateFromInputs(); }); - mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.payid")); + InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.payid")); mobileNrInputTextField.setValidator(australiaPayidValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { - australiaPayid.setPayid(newValue); + australiaPayidAccount.setPayid(newValue); updateFromInputs(); }); - TradeCurrency singleTradeCurrency = australiaPayid.getSingleTradeCurrency(); + TradeCurrency singleTradeCurrency = australiaPayidAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(false); @@ -91,22 +89,21 @@ public class AustraliaPayidForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(mobileNrInputTextField.getText()); + setAccountNameWithString(australiaPayidAccount.getPayid()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - australiaPayid.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), - Res.get(australiaPayid.getPaymentMethod().getId())); + Res.get(australiaPayidAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.payid"), - australiaPayid.getPayid()); + australiaPayidAccount.getPayid()); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), - australiaPayid.getBankAccountName()).second; + australiaPayidAccount.getBankAccountName()).second; field.setMouseTransparent(false); - TradeCurrency singleTradeCurrency = australiaPayid.getSingleTradeCurrency(); + TradeCurrency singleTradeCurrency = australiaPayidAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); @@ -115,8 +112,8 @@ public class AustraliaPayidForm extends PaymentMethodForm { @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() - && australiaPayidValidator.validate(australiaPayid.getPayid()).isValid - && inputValidator.validate(australiaPayid.getBankAccountName()).isValid - && australiaPayid.getTradeCurrencies().size() > 0); + && australiaPayidValidator.validate(australiaPayidAccount.getPayid()).isValid + && inputValidator.validate(australiaPayidAccount.getBankAccountName()).isValid + && australiaPayidAccount.getTradeCurrencies().size() > 0); } } diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/BankForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/BankForm.java index 0c7138df90..87277bd15c 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/BankForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/BankForm.java @@ -19,7 +19,6 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.GUIUtil; -import bisq.desktop.util.Layout; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.BankUtil; @@ -173,12 +172,11 @@ abstract class BankForm extends GeneralBankForm { } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; String countryCode = bankAccountPayload.getCountryCode(); - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - paymentAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), @@ -398,17 +396,12 @@ abstract class BankForm extends GeneralBankForm { }); } - @Override - protected void autoFillNameTextField() { - autoFillAccountTextFields(bankAccountPayload); - } - @Override public void updateAllInputsValid() { boolean result = isAccountNameValid() && paymentAccount.getSingleTradeCurrency() != null && getCountryBasedPaymentAccount().getCountry() != null - && holderNameInputTextField.getValidator().validate(bankAccountPayload.getHolderName()).isValid; + && inputValidator.validate(bankAccountPayload.getHolderName()).isValid; String countryCode = bankAccountPayload.getCountryCode(); result = getValidationResult(result, countryCode, diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/BizumForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/BizumForm.java new file mode 100644 index 0000000000..34130c1596 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/BizumForm.java @@ -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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; +import bisq.core.payment.BizumAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.BizumAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; + +public class BizumForm extends PaymentMethodForm { + private final BizumAccount account; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.mobile"), + ((BizumAccountPayload) paymentAccountPayload).getMobileNr(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); + return gridRow; + } + + public BizumForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (BizumAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + // this payment method is only for Spain/EUR + account.setSingleTradeCurrency(account.getSupportedCurrencies().get(0)); + CountryUtil.findCountryByCode("ES").ifPresent(c -> account.setCountry(c)); + + gridRowFrom = gridRow + 1; + + InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.mobile")); + mobileNrInputTextField.setValidator(inputValidator); + mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setMobileNr(newValue.trim()); + updateFromInputs(); + }); + + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getMobileNr()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), + account.getMobileNr()).second; + field.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && inputValidator.validate(account.getMobileNr()).isValid); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/CapitualForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/CapitualForm.java new file mode 100644 index 0000000000..939f630ace --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/CapitualForm.java @@ -0,0 +1,120 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.validation.CapitualValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.payment.CapitualAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.CapitualAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.common.util.Tuple2; + +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelFlowPane; + +public class CapitualForm extends PaymentMethodForm { + private final CapitualAccount capitualAccount; + private final CapitualValidator capitualValidator; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.capitual.cap"), + ((CapitualAccountPayload) paymentAccountPayload).getAccountNr()); + return gridRow; + } + + public CapitualForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, + CapitualValidator capitualValidator, + InputValidator inputValidator, + GridPane gridPane, + int gridRow, + CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.capitualAccount = (CapitualAccount) paymentAccount; + this.capitualValidator = capitualValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField accountNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.capitual.cap")); + accountNrInputTextField.setValidator(capitualValidator); + accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + capitualAccount.setAccountNr(newValue); + updateFromInputs(); + }); + + addCurrenciesGrid(true); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + private void addCurrenciesGrid(boolean isEditable) { + final Tuple2 labelFlowPaneTuple2 = addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrencies"), 0); + + FlowPane flowPane = labelFlowPaneTuple2.second; + + if (isEditable) + flowPane.setId("flow-pane-checkboxes-bg"); + else + flowPane.setId("flow-pane-checkboxes-non-editable-bg"); + + paymentAccount.getSupportedCurrencies().forEach(e -> + fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, capitualAccount)); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(capitualAccount.getAccountNr()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(capitualAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.capitual.cap"), + capitualAccount.getAccountNr()); + addLimitations(true); + addCurrenciesGrid(false); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && capitualValidator.validate(capitualAccount.getAccountNr()).isValid + && !capitualAccount.getTradeCurrencies().isEmpty()); + } + +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/CashByMailForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/CashByMailForm.java index 37a8be1edc..7a05fd19c5 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/CashByMailForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/CashByMailForm.java @@ -1,17 +1,18 @@ -/* This file is part of Bisq. +/* + * This file is part of Haveno. * - * Bisq is free software: you can redistribute it and/or modify it + * 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. * - * Bisq is distributed in the hope that it will be useful, but WITHOUT + * 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 Bisq. If not, see . + * along with Haveno. If not, see . */ package bisq.desktop.components.paymentmethods; @@ -42,7 +43,6 @@ import static bisq.desktop.util.FormBuilder.*; public class CashByMailForm extends PaymentMethodForm { private final CashByMailAccount cashByMailAccount; private TextArea postalAddressTextArea; - private InputTextField contactField; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { @@ -78,7 +78,7 @@ public class CashByMailForm extends PaymentMethodForm { addTradeCurrencyComboBox(); currencyComboBox.setItems(FXCollections.observableArrayList(CurrencyUtil.getAllSortedFiatCurrencies())); - contactField = addInputTextField(gridPane, ++gridRow, + InputTextField contactField = addInputTextField(gridPane, ++gridRow, Res.get("payment.cashByMail.contact")); contactField.setPromptText(Res.get("payment.cashByMail.contact.prompt")); contactField.setValidator(inputValidator); @@ -110,14 +110,13 @@ public class CashByMailForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(contactField.getText()); + setAccountNameWithString(cashByMailAccount.getContact()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - cashByMailAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(cashByMailAccount.getPaymentMethod().getId())); @@ -143,7 +142,7 @@ public class CashByMailForm extends PaymentMethodForm { @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() - && !postalAddressTextArea.getText().isEmpty() + && !cashByMailAccount.getPostalAddress().isEmpty() && inputValidator.validate(cashByMailAccount.getContact()).isValid && paymentAccount.getSingleTradeCurrency() != null); } diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/CashDepositForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/CashDepositForm.java index 94f18d2c5b..b3ad552833 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/CashDepositForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/CashDepositForm.java @@ -19,7 +19,6 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.GUIUtil; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.EmailValidator; import bisq.core.account.witness.AccountAgeWitnessService; @@ -190,11 +189,11 @@ public class CashDepositForm extends GeneralBankForm { } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; String countryCode = cashDepositAccountPayload.getCountryCode(); - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), paymentAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), @@ -445,18 +444,13 @@ public class CashDepositForm extends GeneralBankForm { }); } - @Override - protected void autoFillNameTextField() { - autoFillAccountTextFields(cashDepositAccountPayload); - } - @Override public void updateAllInputsValid() { boolean result = isAccountNameValid() && paymentAccount.getSingleTradeCurrency() != null && getCountryBasedPaymentAccount().getCountry() != null - && holderNameInputTextField.getValidator().validate(cashDepositAccountPayload.getHolderName()).isValid - && emailInputTextField.getValidator().validate(cashDepositAccountPayload.getHolderEmail()).isValid; + && inputValidator.validate(cashDepositAccountPayload.getHolderName()).isValid + && emailValidator.validate(cashDepositAccountPayload.getHolderEmail()).isValid; String countryCode = cashDepositAccountPayload.getCountryCode(); result = getValidationResult(result, countryCode, diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/CelPayForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/CelPayForm.java new file mode 100644 index 0000000000..e4f2167460 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/CelPayForm.java @@ -0,0 +1,113 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.validation.EmailValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.payment.CelPayAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.CelPayAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; + +public class CelPayForm extends PaymentMethodForm { + private final CelPayAccount account; + private final EmailValidator validator = new EmailValidator(); + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email"), + ((CelPayAccountPayload) paymentAccountPayload).getEmail()); + return gridRow; + } + + public CelPayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (CelPayAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); + emailInputTextField.setValidator(validator); + emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setEmail(newValue.trim()); + updateFromInputs(); + }); + + addCurrenciesGrid(true); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + private void addCurrenciesGrid(boolean isEditable) { + FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, + Res.get("payment.celpay.supportedCurrenciesForReceiver"), 20, 20).second; + + if (isEditable) { + flowPane.setId("flow-pane-checkboxes-bg"); + } else { + flowPane.setId("flow-pane-checkboxes-non-editable-bg"); + } + + account.getSupportedCurrencies().forEach(currency -> + fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getEmail()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), + account.getEmail()).second; + field.setMouseTransparent(false); + addLimitations(true); + addCurrenciesGrid(false); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && account.getEmail() != null + && validator.validate(account.getEmail()).isValid + && account.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/ChaseQuickPayForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/ChaseQuickPayForm.java index da93200fc5..f8ca113132 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/ChaseQuickPayForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/ChaseQuickPayForm.java @@ -19,7 +19,6 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.ChaseQuickPayValidator; import bisq.core.account.witness.AccountAgeWitnessService; @@ -42,7 +41,6 @@ public class ChaseQuickPayForm extends PaymentMethodForm { private final ChaseQuickPayAccount chaseQuickPayAccount; private final ChaseQuickPayValidator chaseQuickPayValidator; - private InputTextField mobileNrInputTextField; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), @@ -71,7 +69,7 @@ public class ChaseQuickPayForm extends PaymentMethodForm { updateFromInputs(); }); - mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); + InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); mobileNrInputTextField.setValidator(chaseQuickPayValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { chaseQuickPayAccount.setEmail(newValue); @@ -87,14 +85,13 @@ public class ChaseQuickPayForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(mobileNrInputTextField.getText()); + setAccountNameWithString(chaseQuickPayAccount.getEmail()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - chaseQuickPayAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(chaseQuickPayAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/ClearXchangeForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/ClearXchangeForm.java index bc6b2b4779..7fa5d92a14 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/ClearXchangeForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/ClearXchangeForm.java @@ -19,7 +19,6 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.ClearXchangeValidator; import bisq.core.account.witness.AccountAgeWitnessService; @@ -42,7 +41,6 @@ import static bisq.desktop.util.FormBuilder.addTopLabelTextField; public class ClearXchangeForm extends PaymentMethodForm { private final ClearXchangeAccount clearXchangeAccount; private final ClearXchangeValidator clearXchangeValidator; - private InputTextField mobileNrInputTextField; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.owner"), @@ -70,7 +68,7 @@ public class ClearXchangeForm extends PaymentMethodForm { updateFromInputs(); }); - mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email.mobile")); mobileNrInputTextField.setValidator(clearXchangeValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { @@ -87,14 +85,13 @@ public class ClearXchangeForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(mobileNrInputTextField.getText()); + setAccountNameWithString(clearXchangeAccount.getEmailOrMobileNr()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - clearXchangeAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(clearXchangeAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/DomesticWireTransferForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/DomesticWireTransferForm.java new file mode 100644 index 0000000000..36e3214bca --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/DomesticWireTransferForm.java @@ -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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Country; +import bisq.core.payment.DomesticWireTransferAccount; +import bisq.core.payment.CountryBasedPaymentAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.BankAccountPayload; +import bisq.core.payment.payload.DomesticWireTransferAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.layout.GridPane; + +public class DomesticWireTransferForm extends GeneralUsBankForm { + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + DomesticWireTransferAccountPayload domesticWireTransferAccountPayload = (DomesticWireTransferAccountPayload) paymentAccountPayload; + return addFormForBuyer(gridPane, gridRow, paymentAccountPayload, null, + domesticWireTransferAccountPayload.getHolderAddress()); + } + + private final DomesticWireTransferAccount domesticWireTransferAccount; + + public DomesticWireTransferForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.domesticWireTransferAccount = (DomesticWireTransferAccount) paymentAccount; + } + + @Override + public void addFormForEditAccount() { + addFormForEditAccount(domesticWireTransferAccount.getPayload(), domesticWireTransferAccount.getPayload().getHolderAddress()); + } + + @Override + public void addFormForAddAccount() { + addFormForAddAccountInternal(domesticWireTransferAccount.getPayload(), domesticWireTransferAccount.getPayload().getHolderAddress()); + } + + @Override + protected void setHolderAddress(String holderAddress) { + domesticWireTransferAccount.getPayload().setHolderAddress(holderAddress); + } + + @Override + protected void maybeAddAccountTypeCombo(BankAccountPayload bankAccountPayload, Country country) { + // DomesticWireTransfer does not use the account type combo + } + + @Override + public void updateAllInputsValid() { + DomesticWireTransferAccountPayload domesticWireTransferAccountPayload = domesticWireTransferAccount.getPayload(); + boolean result = isAccountNameValid() + && paymentAccount.getSingleTradeCurrency() != null + && ((CountryBasedPaymentAccount) this.paymentAccount).getCountry() != null + && inputValidator.validate(domesticWireTransferAccountPayload.getHolderName()).isValid + && inputValidator.validate(domesticWireTransferAccountPayload.getHolderAddress()).isValid; + + result = getValidationResult(result, + domesticWireTransferAccountPayload.getCountryCode(), + domesticWireTransferAccountPayload.getBankName(), + domesticWireTransferAccountPayload.getBankId(), + domesticWireTransferAccountPayload.getBranchId(), + domesticWireTransferAccountPayload.getAccountNr(), + domesticWireTransferAccountPayload.getAccountNr(), + domesticWireTransferAccountPayload.getHolderTaxId(), + domesticWireTransferAccountPayload.getNationalAccountId()); + allInputsValid.set(result); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/F2FForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/F2FForm.java index cddb712dd4..93c8eceb8f 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/F2FForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/F2FForm.java @@ -18,9 +18,7 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; -import bisq.desktop.util.FormBuilder; import bisq.desktop.util.GUIUtil; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.F2FValidator; import bisq.core.account.witness.AccountAgeWitnessService; @@ -52,7 +50,6 @@ import static bisq.desktop.util.FormBuilder.*; public class F2FForm extends PaymentMethodForm { private final F2FAccount f2fAccount; private final F2FValidator f2fValidator; - private InputTextField cityInputTextField; private Country selectedCountry; public static int addFormForBuyer(GridPane gridPane, int gridRow, @@ -90,7 +87,7 @@ public class F2FForm extends PaymentMethodForm { currencyComboBox = tuple.first; gridRow = tuple.second; - InputTextField contactInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + InputTextField contactInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.f2f.contact")); contactInputTextField.setPromptText(Res.get("payment.f2f.contact.prompt")); contactInputTextField.setValidator(f2fValidator); @@ -99,7 +96,7 @@ public class F2FForm extends PaymentMethodForm { updateFromInputs(); }); - cityInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + InputTextField cityInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.f2f.city")); cityInputTextField.setPromptText(Res.get("payment.f2f.city.prompt")); cityInputTextField.setValidator(f2fValidator); @@ -143,15 +140,14 @@ public class F2FForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(cityInputTextField.getText()); + setAccountNameWithString(f2fAccount.getCity()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - paymentAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/FasterPaymentsForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/FasterPaymentsForm.java index b606866e81..153043c0ff 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/FasterPaymentsForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/FasterPaymentsForm.java @@ -19,7 +19,6 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.AccountNrValidator; import bisq.desktop.util.validation.BranchIdValidator; @@ -61,6 +60,8 @@ public class FasterPaymentsForm extends PaymentMethodForm { private InputTextField holderNameInputTextField; private InputTextField accountNrInputTextField; private InputTextField sortCodeInputTextField; + private final BranchIdValidator branchIdValidator; + private final AccountNrValidator accountNrValidator; public FasterPaymentsForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, @@ -70,6 +71,8 @@ public class FasterPaymentsForm extends PaymentMethodForm { CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.fasterPaymentsAccount = (FasterPaymentsAccount) paymentAccount; + this.branchIdValidator = new BranchIdValidator("GB"); + this.accountNrValidator = new AccountNrValidator("GB"); } @Override @@ -85,14 +88,14 @@ public class FasterPaymentsForm extends PaymentMethodForm { // do not translate as it is used in English only sortCodeInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, UK_SORT_CODE); sortCodeInputTextField.setValidator(inputValidator); - sortCodeInputTextField.setValidator(new BranchIdValidator("GB")); + sortCodeInputTextField.setValidator(branchIdValidator); sortCodeInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { fasterPaymentsAccount.setSortCode(newValue); updateFromInputs(); }); accountNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.accountNr")); - accountNrInputTextField.setValidator(new AccountNrValidator("GB")); + accountNrInputTextField.setValidator(accountNrValidator); accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { fasterPaymentsAccount.setAccountNr(newValue); updateFromInputs(); @@ -108,14 +111,13 @@ public class FasterPaymentsForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(accountNrInputTextField.getText()); + setAccountNameWithString(fasterPaymentsAccount.getAccountNr()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - fasterPaymentsAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(fasterPaymentsAccount.getPaymentMethod().getId())); if (!fasterPaymentsAccount.getHolderName().isEmpty()) { @@ -136,9 +138,9 @@ public class FasterPaymentsForm extends PaymentMethodForm { @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() - && holderNameInputTextField.getValidator().validate(fasterPaymentsAccount.getHolderName()).isValid - && sortCodeInputTextField.getValidator().validate(fasterPaymentsAccount.getSortCode()).isValid - && accountNrInputTextField.getValidator().validate(fasterPaymentsAccount.getAccountNr()).isValid + && inputValidator.validate(fasterPaymentsAccount.getHolderName()).isValid + && branchIdValidator.validate(fasterPaymentsAccount.getSortCode()).isValid + && accountNrValidator.validate(fasterPaymentsAccount.getAccountNr()).isValid && fasterPaymentsAccount.getTradeCurrencies().size() > 0); } } diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralAccountNumberForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralAccountNumberForm.java index 8d620a0996..249813e871 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralAccountNumberForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralAccountNumberForm.java @@ -1,13 +1,11 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; -import bisq.desktop.util.Layout; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.payment.PaymentAccount; -import bisq.core.payment.payload.PaymentMethod; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.InputValidator; @@ -20,8 +18,6 @@ import static bisq.desktop.util.FormBuilder.addTopLabelTextField; public abstract class GeneralAccountNumberForm extends PaymentMethodForm { - private InputTextField accountNrInputTextField; - GeneralAccountNumberForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); } @@ -30,7 +26,7 @@ public abstract class GeneralAccountNumberForm extends PaymentMethodForm { public void addFormForAddAccount() { gridRowFrom = gridRow + 1; - accountNrInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.account.no")); + InputTextField accountNrInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.account.no")); accountNrInputTextField.setValidator(inputValidator); accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { setAccountNumber(newValue); @@ -51,13 +47,21 @@ public abstract class GeneralAccountNumberForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(accountNrInputTextField.getText()); + setAccountNameWithString(getAccountNr()); } @Override - public void addFormForDisplayAccount() { - addFormForAccountNumberDisplayAccount(paymentAccount.getAccountName(), paymentAccount.getPaymentMethod(), getAccountNr(), - paymentAccount.getSingleTradeCurrency()); + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.no"), getAccountNr()).second; + field.setMouseTransparent(false); + + final String nameAndCode = paymentAccount.getSingleTradeCurrency() != null ? paymentAccount.getSingleTradeCurrency().getNameAndCode() : ""; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + + addLimitations(true); } @@ -71,18 +75,4 @@ public abstract class GeneralAccountNumberForm extends PaymentMethodForm { abstract void setAccountNumber(String newValue); abstract String getAccountNr(); - - private void addFormForAccountNumberDisplayAccount(String accountName, PaymentMethod paymentMethod, String accountNr, - TradeCurrency singleTradeCurrency) { - gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), accountName, Layout.FIRST_ROW_AND_GROUP_DISTANCE); - addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentMethod.getId())); - TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.no"), accountNr).second; - field.setMouseTransparent(false); - - final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; - addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); - - addLimitations(true); - } } diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralBankForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralBankForm.java index 2889abc130..75d76c81bf 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralBankForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralBankForm.java @@ -10,7 +10,7 @@ import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.BankUtil; import bisq.core.locale.Res; import bisq.core.payment.PaymentAccount; -import bisq.core.payment.payload.CountryBasedPaymentAccountPayload; +import bisq.core.payment.payload.BankAccountPayload; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.InputValidator; @@ -135,27 +135,29 @@ public abstract class GeneralBankForm extends PaymentMethodForm { } } - void autoFillAccountTextFields(CountryBasedPaymentAccountPayload paymentAccountPayload) { + @Override + protected void autoFillNameTextField() { if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { + BankAccountPayload payload = (BankAccountPayload) paymentAccount.paymentAccountPayload; String bankId = null; - String countryCode = paymentAccountPayload.getCountryCode(); + String countryCode = payload.getCountryCode(); if (countryCode == null) countryCode = ""; if (BankUtil.isBankIdRequired(countryCode)) { - bankId = bankIdInputTextField.getText().trim(); + bankId = payload.getBankId(); if (bankId.length() > 9) bankId = StringUtils.abbreviate(bankId, 9); } else if (BankUtil.isBranchIdRequired(countryCode)) { - bankId = branchIdInputTextField.getText().trim(); + bankId = payload.getBranchId(); if (bankId.length() > 9) bankId = StringUtils.abbreviate(bankId, 9); } else if (BankUtil.isBankNameRequired(countryCode)) { - bankId = bankNameInputTextField.getText().trim(); + bankId = payload.getBankName(); if (bankId.length() > 9) bankId = StringUtils.abbreviate(bankId, 9); } - String accountNr = accountNrInputTextField.getText().trim(); + String accountNr = payload.getAccountNr(); if (accountNr.length() > 9) accountNr = StringUtils.abbreviate(accountNr, 9); @@ -201,7 +203,7 @@ public abstract class GeneralBankForm extends PaymentMethodForm { if (BankUtil.isNationalAccountIdRequired(countryCode)) result = result && nationalAccountIdInputTextField.getValidator().validate(nationalAccountId).isValid; } else { // only account number not empty validation - result = result && accountNrInputTextField.getValidator().validate(accountNr).isValid; + result = result && (accountNrInputTextField == null || accountNrInputTextField.getValidator().validate(accountNr).isValid); } return result; diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralSepaForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralSepaForm.java index 5435e9e4c9..f09c936fb1 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralSepaForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralSepaForm.java @@ -1,12 +1,10 @@ package bisq.desktop.components.paymentmethods; -import bisq.desktop.components.InputTextField; +import bisq.desktop.components.AutoTooltipCheckBox; import bisq.desktop.util.FormBuilder; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.Country; -import bisq.core.locale.CurrencyUtil; -import bisq.core.locale.FiatCurrency; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.payment.CountryBasedPaymentAccount; @@ -22,14 +20,15 @@ import com.jfoenix.controls.JFXTextField; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.util.StringConverter; -import java.util.ArrayList; import java.util.List; +import java.util.Objects; import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox; @@ -38,16 +37,8 @@ public abstract class GeneralSepaForm extends PaymentMethodForm { static final String BIC = "BIC"; static final String IBAN = "IBAN"; - final List euroCountryCheckBoxes = new ArrayList<>(); - final List nonEuroCountryCheckBoxes = new ArrayList<>(); - private TextField currencyTextField; - InputTextField ibanInputTextField; - - private FiatCurrency euroCurrency = CurrencyUtil.getFiatCurrency("EUR").get(); - GeneralSepaForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); - paymentAccount.setSingleTradeCurrency(euroCurrency); } @Override @@ -56,7 +47,7 @@ public abstract class GeneralSepaForm extends PaymentMethodForm { TradeCurrency singleTradeCurrency = this.paymentAccount.getSingleTradeCurrency(); String currency = singleTradeCurrency != null ? singleTradeCurrency.getCode() : null; if (currency != null) { - String iban = ibanInputTextField.getText(); + String iban = getIban(); if (iban.length() > 9) iban = StringUtils.abbreviate(iban, 9); String method = Res.get(paymentAccount.getPaymentMethod().getId()); @@ -75,21 +66,34 @@ public abstract class GeneralSepaForm extends PaymentMethodForm { Country selectedItem = countryComboBox.getSelectionModel().getSelectedItem(); paymentAccount.setCountry(selectedItem); - updateCountriesSelection(euroCountryCheckBoxes); - updateCountriesSelection(nonEuroCountryCheckBoxes); updateFromInputs(); }); } - void addCountriesGrid(String title, List checkBoxList, - List dataProvider) { + void addCountriesGrid(String title, List countries) { FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, title, 0).second; flowPane.setId("flow-pane-checkboxes-bg"); - dataProvider.forEach(country -> - fillUpFlowPaneWithCountries(checkBoxList, flowPane, country)); - updateCountriesSelection(checkBoxList); + countries.forEach(country -> { + CheckBox checkBox = new AutoTooltipCheckBox(country.code); + checkBox.setUserData(country.code); + checkBox.setSelected(isCountryAccepted(country.code)); + checkBox.setMouseTransparent(false); + checkBox.setMinWidth(45); + checkBox.setMaxWidth(45); + checkBox.setTooltip(new Tooltip(country.name)); + checkBox.setOnAction(event -> { + if (checkBox.isSelected()) { + addAcceptedCountry(country.code); + } else { + removeAcceptedCountry(country.code); + } + + updateAllInputsValid(); + }); + flowPane.getChildren().add(checkBox); + }); } ComboBox addCountrySelection() { @@ -97,7 +101,7 @@ public abstract class GeneralSepaForm extends PaymentMethodForm { hBox.setSpacing(10); ComboBox countryComboBox = new JFXComboBox<>(); - currencyTextField = new JFXTextField(""); + TextField currencyTextField = new JFXTextField(""); currencyTextField.setEditable(false); currencyTextField.setMouseTransparent(true); currencyTextField.setFocusTraversable(false); @@ -105,7 +109,8 @@ public abstract class GeneralSepaForm extends PaymentMethodForm { currencyTextField.setVisible(true); currencyTextField.setManaged(true); - currencyTextField.setText(Res.get("payment.currencyWithSymbol", euroCurrency.getNameAndCode())); + currencyTextField.setText(Res.get("payment.currencyWithSymbol", + Objects.requireNonNull(paymentAccount.getSingleTradeCurrency()).getNameAndCode())); hBox.getChildren().addAll(countryComboBox, currencyTextField); @@ -126,6 +131,7 @@ public abstract class GeneralSepaForm extends PaymentMethodForm { return countryComboBox; } - abstract void updateCountriesSelection(List checkBoxList); + abstract boolean isCountryAccepted(String countryCode); + protected abstract String getIban(); } diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralUsBankForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralUsBankForm.java new file mode 100644 index 0000000000..ebaca47b7b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralUsBankForm.java @@ -0,0 +1,164 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.BankUtil; +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.CountryBasedPaymentAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.BankAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextArea; +import javafx.scene.layout.GridPane; + +import javax.annotation.Nullable; + +import static bisq.common.util.Utilities.cleanString; +import static bisq.desktop.util.FormBuilder.*; + +public abstract class GeneralUsBankForm extends GeneralBankForm { + + protected static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload, + @Nullable String accountType, + String holderAddress) { + BankAccountPayload bankAccountPayload = (BankAccountPayload) paymentAccountPayload; + String countryCode = bankAccountPayload.getCountryCode(); + int colIndex = 1; + + addTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + Res.get("payment.account.owner"), bankAccountPayload.getHolderName(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); + + String branchIdLabel = BankUtil.getBranchIdLabel(countryCode); + String accountNrLabel = BankUtil.getAccountNrLabel(countryCode); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + branchIdLabel + " / " + accountNrLabel, + bankAccountPayload.getBranchId() + " / " + bankAccountPayload.getAccountNr()); + + String bankNameLabel = BankUtil.getBankNameLabel(countryCode); + String accountTypeLabel = accountType == null ? "" : " / " + BankUtil.getAccountTypeLabel(countryCode); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + bankNameLabel + accountTypeLabel, + accountType == null ? bankAccountPayload.getBankName() : bankAccountPayload.getBankName() + " / " + accountType); + + if (holderAddress.length() > 0) { + TextArea textAddress = addCompactTopLabelTextArea(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), + Res.get("payment.account.address"), "").second; + textAddress.setMinHeight(70); + textAddress.setEditable(false); + textAddress.setText(holderAddress); + } + return gridRow; + } + + public GeneralUsBankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + } + + protected void addFormForEditAccount(BankAccountPayload bankAccountPayload, String holderAddress) { + Country country = ((CountryBasedPaymentAccount) paymentAccount).getCountry(); + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(paymentAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), bankAccountPayload.getHolderName()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.address"), cleanString(holderAddress)); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.bank.name"), bankAccountPayload.getBankName()); + addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel(country.code), bankAccountPayload.getBranchId()); + addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel(country.code), bankAccountPayload.getAccountNr()); + if (bankAccountPayload.getAccountType() != null) { + addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountTypeLabel(country.code), bankAccountPayload.getAccountType()); + } + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), paymentAccount.getSingleTradeCurrency().getNameAndCode()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), country.name); + addLimitations(true); + } + + protected void addFormForAddAccountInternal(BankAccountPayload bankAccountPayload, String holderAddress) { + // this payment method is only for United States/USD + CountryUtil.findCountryByCode("US").ifPresent(c -> onCountrySelected(c)); + Country country = ((CountryBasedPaymentAccount) paymentAccount).getCountry(); + + InputTextField holderNameInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner")); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + bankAccountPayload.setHolderName(newValue); + updateFromInputs(); + }); + holderNameInputTextField.setValidator(inputValidator); + + TextArea addressTextArea = addTopLabelTextArea(gridPane, ++gridRow, + Res.get("payment.account.owner.address"), Res.get("payment.account.owner.address")).second; + addressTextArea.setMinHeight(70); + addressTextArea.textProperty().addListener((ov, oldValue, newValue) -> { + setHolderAddress(newValue.trim()); + updateFromInputs(); + }); + + bankNameInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.bank.name")); + bankNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + bankAccountPayload.setBankName(newValue); + updateFromInputs(); + }); + bankNameInputTextField.setValidator(inputValidator); + + branchIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel(country.code)); + branchIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + bankAccountPayload.setBranchId(newValue); + updateFromInputs(); + }); + branchIdInputTextField.setValidator(inputValidator); + + accountNrInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel(country.code)); + accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + bankAccountPayload.setAccountNr(newValue); + updateFromInputs(); + }); + accountNrInputTextField.setValidator(inputValidator); + + maybeAddAccountTypeCombo(bankAccountPayload, country); + + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), paymentAccount.getSingleTradeCurrency().getNameAndCode()); + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), country.name); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + this.validatorsApplied = true; + } + + abstract protected void setHolderAddress(String holderAddress); + + abstract protected void maybeAddAccountTypeCombo(BankAccountPayload bankAccountPayload, Country country); + + protected void onCountrySelected(Country country) { + if (country != null) { + ((CountryBasedPaymentAccount) this.paymentAccount).setCountry(country); + String countryCode = country.code; + TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode); + paymentAccount.setSingleTradeCurrency(currency); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/HalCashForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/HalCashForm.java index 316cac9d72..b605e1945b 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/HalCashForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/HalCashForm.java @@ -19,7 +19,6 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.HalCashValidator; import bisq.core.account.witness.AccountAgeWitnessService; @@ -41,7 +40,6 @@ import static bisq.desktop.util.FormBuilder.addTopLabelTextField; public class HalCashForm extends PaymentMethodForm { private final HalCashAccount halCashAccount; private final HalCashValidator halCashValidator; - private InputTextField mobileNrInputTextField; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { @@ -61,7 +59,7 @@ public class HalCashForm extends PaymentMethodForm { public void addFormForAddAccount() { gridRowFrom = gridRow + 1; - mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.mobile")); mobileNrInputTextField.setValidator(halCashValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { @@ -78,14 +76,13 @@ public class HalCashForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(mobileNrInputTextField.getText()); + setAccountNameWithString(halCashAccount.getMobileNr()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - halCashAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(halCashAccount.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/IfscBankForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/IfscBankForm.java new file mode 100644 index 0000000000..6961f187fb --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/IfscBankForm.java @@ -0,0 +1,130 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.IfscBasedAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; +import bisq.core.util.validation.RegexValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; + +public class IfscBankForm extends PaymentMethodForm { + private final IfscBasedAccountPayload ifscBasedAccountPayload; + private final RegexValidator ifscValidator; // https://en.wikipedia.org/wiki/Indian_Financial_System_Code + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + IfscBasedAccountPayload ifscAccountPayload = (IfscBasedAccountPayload) paymentAccountPayload; + addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.account.owner"), ifscAccountPayload.getHolderName(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.accountNr"), ifscAccountPayload.getAccountNr()); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.ifsc"), ifscAccountPayload.getIfsc()); + return gridRow; + } + + public IfscBankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.ifscBasedAccountPayload = (IfscBasedAccountPayload) paymentAccount.paymentAccountPayload; + ifscValidator = new RegexValidator(); + ifscValidator.setPattern("[A-Z]{4}0[0-9]{6}"); + ifscValidator.setErrorMessage(Res.get("payment.ifsc.validation")); + } + + @Override + public void addFormForAddAccount() { + // this payment method is only for India/INR + paymentAccount.setSingleTradeCurrency(paymentAccount.getSupportedCurrencies().get(0)); + CountryUtil.findCountryByCode("IN").ifPresent(c -> ifscBasedAccountPayload.setCountryCode(c.code)); + + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.account.owner")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + ifscBasedAccountPayload.setHolderName(newValue.trim()); + updateFromInputs(); + }); + + InputTextField accountNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.accountNr")); + accountNrInputTextField.setValidator(inputValidator); + accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + ifscBasedAccountPayload.setAccountNr(newValue.trim()); + updateFromInputs(); + }); + + InputTextField ifscInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.ifsc")); + ifscInputTextField.setText("XXXX0999999"); + ifscInputTextField.setValidator(ifscValidator); + ifscInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + ifscBasedAccountPayload.setIfsc(newValue.trim()); + updateFromInputs(); + }); + + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), paymentAccount.getSingleTradeCurrency().getNameAndCode()); + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), CountryUtil.getNameByCode(ifscBasedAccountPayload.getCountryCode())); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(ifscBasedAccountPayload.getHolderName()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + ifscBasedAccountPayload.getHolderName()); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.accountNr"), ifscBasedAccountPayload.getAccountNr()).second; + field.setMouseTransparent(false); + TextField fieldIfsc = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.ifsc"), ifscBasedAccountPayload.getIfsc()).second; + fieldIfsc.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), paymentAccount.getSingleTradeCurrency().getNameAndCode()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), CountryUtil.getNameByCode(ifscBasedAccountPayload.getCountryCode())); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && inputValidator.validate(ifscBasedAccountPayload.getHolderName()).isValid + && inputValidator.validate(ifscBasedAccountPayload.getAccountNr()).isValid + && ifscValidator.validate(ifscBasedAccountPayload.getIfsc()).isValid); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/ImpsForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/ImpsForm.java new file mode 100644 index 0000000000..21245b94dc --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/ImpsForm.java @@ -0,0 +1,38 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.layout.GridPane; + +public class ImpsForm extends IfscBankForm { + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + return IfscBankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + } + + public ImpsForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/InteracETransferForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/InteracETransferForm.java index b067ff9d44..b91793e98f 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/InteracETransferForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/InteracETransferForm.java @@ -19,7 +19,6 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.InteracETransferValidator; import bisq.core.account.witness.AccountAgeWitnessService; @@ -41,7 +40,6 @@ public class InteracETransferForm extends PaymentMethodForm { private final InteracETransferAccount interacETransferAccount; private final InteracETransferValidator interacETransferValidator; - private InputTextField mobileNrInputTextField; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { @@ -75,7 +73,7 @@ public class InteracETransferForm extends PaymentMethodForm { updateFromInputs(); }); - mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.emailOrMobile")); + InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.emailOrMobile")); mobileNrInputTextField.setValidator(interacETransferValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { interacETransferAccount.setEmail(newValue); @@ -105,14 +103,13 @@ public class InteracETransferForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(mobileNrInputTextField.getText()); + setAccountNameWithString(interacETransferAccount.getEmail()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - interacETransferAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(interacETransferAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/JapanBankTransferForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/JapanBankTransferForm.java index 6612b78e7b..40bad1b307 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/JapanBankTransferForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/JapanBankTransferForm.java @@ -20,12 +20,10 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.AutocompleteComboBox; import bisq.desktop.components.InputTextField; import bisq.desktop.components.paymentmethods.data.JapanBankData; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.JapanBankAccountNameValidator; import bisq.desktop.util.validation.JapanBankAccountNumberValidator; import bisq.desktop.util.validation.JapanBankBranchCodeValidator; import bisq.desktop.util.validation.JapanBankBranchNameValidator; -import bisq.desktop.util.validation.JapanBankTransferValidator; import bisq.desktop.util.validation.LengthValidator; import bisq.core.account.witness.AccountAgeWitnessService; @@ -57,24 +55,17 @@ import javafx.util.StringConverter; import static bisq.desktop.util.FormBuilder.*; import static bisq.desktop.util.GUIUtil.getComboBoxButtonCell; -public class JapanBankTransferForm extends PaymentMethodForm -{ +public class JapanBankTransferForm extends PaymentMethodForm { private final JapanBankAccount japanBankAccount; - protected ComboBox bankComboBox, bankAccountTypeComboBox; - private InputTextField bankAccountNumberInputTextField; + protected ComboBox bankComboBox; - private JapanBankTransferValidator japanBankTransferValidator; - private JapanBankBranchNameValidator japanBankBranchNameValidator; - private JapanBankBranchCodeValidator japanBankBranchCodeValidator; - private JapanBankAccountNameValidator japanBankAccountNameValidator; - private JapanBankAccountNumberValidator japanBankAccountNumberValidator; + private final JapanBankBranchNameValidator japanBankBranchNameValidator; + private final JapanBankBranchCodeValidator japanBankBranchCodeValidator; + private final JapanBankAccountNameValidator japanBankAccountNameValidator; + private final JapanBankAccountNumberValidator japanBankAccountNumberValidator; - private LengthValidator lengthValidator; - private RegexValidator regexValidator; - - public static int addFormForBuyer(GridPane gridPane, int gridRow, // {{{ - PaymentAccountPayload paymentAccountPayload) - { + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { JapanBankAccountPayload japanBankAccount = ((JapanBankAccountPayload) paymentAccountPayload); String bankText = japanBankAccount.getBankCode() + " " + japanBankAccount.getBankName(); @@ -90,34 +81,29 @@ public class JapanBankTransferForm extends PaymentMethodForm addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.japan.recipient"), accountNameText); return gridRow; - } // }}} + } public JapanBankTransferForm(PaymentAccount paymentAccount, - AccountAgeWitnessService accountAgeWitnessService, - JapanBankTransferValidator japanBankTransferValidator, - InputValidator inputValidator, GridPane gridPane, - int gridRow, CoinFormatter formatter) - { + AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.japanBankAccount = (JapanBankAccount) paymentAccount; - this.japanBankTransferValidator = japanBankTransferValidator; this.japanBankBranchCodeValidator = new JapanBankBranchCodeValidator(); this.japanBankAccountNumberValidator = new JapanBankAccountNumberValidator(); - this.lengthValidator = new LengthValidator(); - this.regexValidator = new RegexValidator(); + LengthValidator lengthValidator = new LengthValidator(); + RegexValidator regexValidator = new RegexValidator(); this.japanBankBranchNameValidator = new JapanBankBranchNameValidator(lengthValidator, regexValidator); this.japanBankAccountNameValidator = new JapanBankAccountNameValidator(lengthValidator, regexValidator); } @Override - public void addFormForDisplayAccount() // {{{ - { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.name"), - japanBankAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(japanBankAccount.getPaymentMethod().getId())); @@ -128,37 +114,36 @@ public class JapanBankTransferForm extends PaymentMethodForm addBankAccountTypeDisplay(); addLimitations(true); - } // }}} - private void addBankDisplay() // {{{ - { + } + + private void addBankDisplay() { String bankText = japanBankAccount.getBankCode() + " " + japanBankAccount.getBankName(); TextField bankTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("bank"), bankText).second; bankTextField.setEditable(false); - } // }}} - private void addBankBranchDisplay() // {{{ - { + } + + private void addBankBranchDisplay() { String branchText = japanBankAccount.getBankBranchCode() + " " + japanBankAccount.getBankBranchName(); TextField branchTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("branch"), branchText).second; branchTextField.setEditable(false); - } // }}} - private void addBankAccountDisplay() // {{{ - { + } + + private void addBankAccountDisplay() { String accountText = japanBankAccount.getBankAccountNumber() + " " + japanBankAccount.getBankAccountName(); TextField accountTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("account"), accountText).second; accountTextField.setEditable(false); - } // }}} - private void addBankAccountTypeDisplay() // {{{ - { + } + + private void addBankAccountTypeDisplay() { TradeCurrency singleTradeCurrency = japanBankAccount.getSingleTradeCurrency(); String currency = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; String accountTypeText = currency + " " + japanBankAccount.getBankAccountType(); TextField accountTypeTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("account.type"), accountTypeText).second; accountTypeTextField.setEditable(false); - } // }}} + } @Override - public void addFormForAddAccount() // {{{ - { + public void addFormForAddAccount() { gridRowFrom = gridRow; addBankInput(); @@ -168,9 +153,9 @@ public class JapanBankTransferForm extends PaymentMethodForm addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); - } // }}} - private void addBankInput() // {{{ - { + } + + private void addBankInput() { gridRow++; Tuple4> tuple4 = addTopLabelTextFieldAutocompleteComboBox(gridPane, gridRow, JapanBankData.getString("bank.code"), JapanBankData.getString("bank.name"), 10); @@ -185,14 +170,13 @@ public class JapanBankTransferForm extends PaymentMethodForm bankComboBox = tuple4.fourth; bankComboBox.setPromptText(JapanBankData.getString("bank.select")); bankComboBox.setButtonCell(getComboBoxButtonCell(JapanBankData.getString("bank.name"), bankComboBox)); - bankComboBox.getEditor().focusedProperty().addListener(observable -> { - bankComboBox.setPromptText(""); - }); - bankComboBox.setConverter(new StringConverter() { + bankComboBox.getEditor().focusedProperty().addListener(observable -> bankComboBox.setPromptText("")); + bankComboBox.setConverter(new StringConverter<>() { @Override public String toString(String bank) { return bank != null ? bank : ""; } + public String fromString(String s) { return s != null ? s : ""; } @@ -208,8 +192,7 @@ public class JapanBankTransferForm extends PaymentMethodForm // parse first 4 characters as bank code String bankCode = StringUtils.substring(bank, 0, 4); - if (bankCode != null) - { + if (bankCode != null) { // set bank code field to this value bankCodeField.setText(bankCode); // save to payload @@ -217,26 +200,20 @@ public class JapanBankTransferForm extends PaymentMethodForm // parse remainder as bank name String bankNameFull = StringUtils.substringAfter(bank, JapanBankData.SPACE); - if (bankNameFull != null) - { - // parse beginning as Japanese bank name - String bankNameJa = StringUtils.substringBefore(bankNameFull, JapanBankData.SPACE); - if (bankNameJa != null) - { - // set bank name field to this value - bankComboBox.getEditor().setText(bankNameJa); - // save to payload - japanBankAccount.setBankName(bankNameJa); - } - } + // parse beginning as Japanese bank name + String bankNameJa = StringUtils.substringBefore(bankNameFull, JapanBankData.SPACE); + // set bank name field to this value + bankComboBox.getEditor().setText(bankNameJa); + // save to payload + japanBankAccount.setBankName(bankNameJa); } updateFromInputs(); }); - } // }}} - private void addBankBranchInput() // {{{ - { + } + + private void addBankBranchInput() { gridRow++; Tuple2 tuple2 = addInputTextFieldInputTextField(gridPane, gridRow, JapanBankData.getString("branch.code"), JapanBankData.getString("branch.name")); @@ -259,14 +236,14 @@ public class JapanBankTransferForm extends PaymentMethodForm japanBankAccount.setBankBranchName(newValue); updateFromInputs(); }); - } // }}} - private void addBankAccountInput() // {{{ - { + } + + private void addBankAccountInput() { gridRow++; Tuple2 tuple2 = addInputTextFieldInputTextField(gridPane, gridRow, JapanBankData.getString("account.number"), JapanBankData.getString("account.name")); // account number - bankAccountNumberInputTextField = tuple2.first; + InputTextField bankAccountNumberInputTextField = tuple2.first; bankAccountNumberInputTextField.setValidator(japanBankAccountNumberValidator); bankAccountNumberInputTextField.setPrefWidth(200); bankAccountNumberInputTextField.setMaxWidth(200); @@ -284,9 +261,9 @@ public class JapanBankTransferForm extends PaymentMethodForm japanBankAccount.setBankAccountName(newValue); updateFromInputs(); }); - } // }}} - private void addBankAccountTypeInput() // {{{ - { + } + + private void addBankAccountTypeInput() { // account currency gridRow++; @@ -299,13 +276,13 @@ public class JapanBankTransferForm extends PaymentMethodForm ToggleGroup toggleGroup = new ToggleGroup(); Tuple3 tuple3 = - addTopLabelRadioButtonRadioButton( - gridPane, gridRow, toggleGroup, - JapanBankData.getString("account.type.select"), - JapanBankData.getString("account.type.futsu"), - JapanBankData.getString("account.type.touza"), - 0 - ); + addTopLabelRadioButtonRadioButton( + gridPane, gridRow, toggleGroup, + JapanBankData.getString("account.type.select"), + JapanBankData.getString("account.type.futsu"), + JapanBankData.getString("account.type.touza"), + 0 + ); toggleGroup.getToggles().get(0).setSelected(true); japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.futsu.ja")); @@ -314,66 +291,60 @@ public class JapanBankTransferForm extends PaymentMethodForm RadioButton touza = tuple3.third; toggleGroup.selectedToggleProperty().addListener - ( - (ov, oldValue, newValue) -> - { - if (futsu.isSelected()) - japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.futsu.ja")); - if (touza.isSelected()) - japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.touza.ja")); - } - ); - } // }}} + ( + (ov, oldValue, newValue) -> + { + if (futsu.isSelected()) + japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.futsu.ja")); + if (touza.isSelected()) + japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.touza.ja")); + } + ); + } @Override - public void updateFromInputs() // {{{ - { + public void updateFromInputs() { System.out.println("JapanBankTransferForm: updateFromInputs()"); - System.out.println("bankName: "+japanBankAccount.getBankName()); - System.out.println("bankCode: "+japanBankAccount.getBankCode()); - System.out.println("bankBranchName: "+japanBankAccount.getBankBranchName()); - System.out.println("bankBranchCode: "+japanBankAccount.getBankBranchCode()); - System.out.println("bankAccountType: "+japanBankAccount.getBankAccountType()); - System.out.println("bankAccountName: "+japanBankAccount.getBankAccountName()); - System.out.println("bankAccountNumber: "+japanBankAccount.getBankAccountNumber()); + System.out.println("bankName: " + japanBankAccount.getBankName()); + System.out.println("bankCode: " + japanBankAccount.getBankCode()); + System.out.println("bankBranchName: " + japanBankAccount.getBankBranchName()); + System.out.println("bankBranchCode: " + japanBankAccount.getBankBranchCode()); + System.out.println("bankAccountType: " + japanBankAccount.getBankAccountType()); + System.out.println("bankAccountName: " + japanBankAccount.getBankAccountName()); + System.out.println("bankAccountNumber: " + japanBankAccount.getBankAccountNumber()); super.updateFromInputs(); - } // }}} + } @Override - protected void autoFillNameTextField() // {{{ - { - if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) - { + protected void autoFillNameTextField() { + if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { accountNameTextField.setText( Res.get(paymentAccount.getPaymentMethod().getId()) - .concat(": ") - .concat(japanBankAccount.getBankName()) - .concat(" ") - .concat(japanBankAccount.getBankBranchName()) - .concat(" ") - .concat(japanBankAccount.getBankAccountNumber()) - .concat(" ") - .concat(japanBankAccount.getBankAccountName()) + .concat(": ") + .concat(japanBankAccount.getBankName()) + .concat(" ") + .concat(japanBankAccount.getBankBranchName()) + .concat(" ") + .concat(japanBankAccount.getBankAccountNumber()) + .concat(" ") + .concat(japanBankAccount.getBankAccountName()) ); } - } // }}} + } @Override - public void updateAllInputsValid() // {{{ - { + public void updateAllInputsValid() { boolean result = - ( - isAccountNameValid() && - inputValidator.validate(japanBankAccount.getBankCode()).isValid && - inputValidator.validate(japanBankAccount.getBankName()).isValid && - japanBankBranchCodeValidator.validate(japanBankAccount.getBankBranchCode()).isValid && - japanBankBranchNameValidator.validate(japanBankAccount.getBankBranchName()).isValid && - japanBankAccountNumberValidator.validate(japanBankAccount.getBankAccountNumber()).isValid && - japanBankAccountNameValidator.validate(japanBankAccount.getBankAccountName()).isValid && - inputValidator.validate(japanBankAccount.getBankAccountType()).isValid - ); + ( + isAccountNameValid() && + inputValidator.validate(japanBankAccount.getBankCode()).isValid && + inputValidator.validate(japanBankAccount.getBankName()).isValid && + japanBankBranchCodeValidator.validate(japanBankAccount.getBankBranchCode()).isValid && + japanBankBranchNameValidator.validate(japanBankAccount.getBankBranchName()).isValid && + japanBankAccountNumberValidator.validate(japanBankAccount.getBankAccountNumber()).isValid && + japanBankAccountNameValidator.validate(japanBankAccount.getBankAccountName()).isValid && + inputValidator.validate(japanBankAccount.getBankAccountType()).isValid + ); allInputsValid.set(result); - } // }}} + } } - -// vim:ts=4:sw=4:expandtab:foldmethod=marker:nowrap: diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneseForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneseForm.java new file mode 100644 index 0000000000..82c501d3a6 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneseForm.java @@ -0,0 +1,120 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.payment.MoneseAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.MoneseAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; + +public class MoneseForm extends PaymentMethodForm { + private final MoneseAccount account; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 0, Res.get("payment.account.owner"), + ((MoneseAccountPayload) paymentAccountPayload).getHolderName()); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.mobile"), + ((MoneseAccountPayload) paymentAccountPayload).getMobileNr()); + return gridRow; + } + + public MoneseForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (MoneseAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setHolderName(newValue.trim()); + updateFromInputs(); + }); + + InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.mobile")); + mobileNrInputTextField.setValidator(inputValidator); + mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setMobileNr(newValue.trim()); + updateFromInputs(); + }); + + addCurrenciesGrid(true); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + private void addCurrenciesGrid(boolean isEditable) { + FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, + Res.get("payment.supportedCurrencies"), 20, 20).second; + + if (isEditable) { + flowPane.setId("flow-pane-checkboxes-bg"); + } else { + flowPane.setId("flow-pane-checkboxes-non-editable-bg"); + } + + account.getSupportedCurrencies().forEach(currency -> + fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getMobileNr()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), account.getHolderName()) + .second.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), account.getMobileNr()) + .second.setMouseTransparent(false); + addLimitations(true); + addCurrenciesGrid(false); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && inputValidator.validate(account.getHolderName()).isValid + && inputValidator.validate(account.getMobileNr()).isValid + && account.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneyBeamForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneyBeamForm.java index a26e2c47e7..cc5282e0a4 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneyBeamForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneyBeamForm.java @@ -19,7 +19,6 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.MoneyBeamValidator; import bisq.core.account.witness.AccountAgeWitnessService; @@ -42,7 +41,6 @@ import static bisq.desktop.util.FormBuilder.addTopLabelTextField; public class MoneyBeamForm extends PaymentMethodForm { private final MoneyBeamAccount account; private final MoneyBeamValidator validator; - private InputTextField accountIdInputTextField; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.moneyBeam.accountId"), ((MoneyBeamAccountPayload) paymentAccountPayload).getAccountId()); @@ -59,7 +57,7 @@ public class MoneyBeamForm extends PaymentMethodForm { public void addFormForAddAccount() { gridRowFrom = gridRow + 1; - accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.moneyBeam.accountId")); + InputTextField accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.moneyBeam.accountId")); accountIdInputTextField.setValidator(validator); accountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setAccountId(newValue.trim()); @@ -75,13 +73,13 @@ public class MoneyBeamForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(accountIdInputTextField.getText()); + setAccountNameWithString(account.getAccountId()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), account.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.moneyBeam.accountId"), account.getAccountId()).second; field.setMouseTransparent(false); diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneyGramForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneyGramForm.java index 53b4747cb5..684354903a 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneyGramForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/MoneyGramForm.java @@ -26,7 +26,6 @@ import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.BankUtil; import bisq.core.locale.Country; import bisq.core.locale.CountryUtil; -import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.payment.MoneyGramAccount; import bisq.core.payment.PaymentAccount; @@ -37,15 +36,16 @@ import bisq.core.util.validation.InputValidator; import bisq.common.util.Tuple2; -import org.apache.commons.lang3.StringUtils; - import javafx.scene.control.Label; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import lombok.extern.slf4j.Slf4j; -import static bisq.desktop.util.FormBuilder.*; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addInputTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelFlowPane; @Slf4j public class MoneyGramForm extends PaymentMethodForm { @@ -69,7 +69,6 @@ public class MoneyGramForm extends PaymentMethodForm { } private final MoneyGramAccountPayload moneyGramAccountPayload; - private InputTextField holderNameInputTextField; private InputTextField stateInputTextField; private final EmailValidator emailValidator; @@ -82,10 +81,10 @@ public class MoneyGramForm extends PaymentMethodForm { } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; final Country country = getMoneyGramPaymentAccount().getCountry(); - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), paymentAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), country != null ? country.name : ""); @@ -106,7 +105,7 @@ public class MoneyGramForm extends PaymentMethodForm { gridRow = GUIUtil.addRegionCountry(gridPane, gridRow, this::onCountrySelected); - holderNameInputTextField = addInputTextField(gridPane, + InputTextField holderNameInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.account.fullName")); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { moneyGramAccountPayload.setHolderName(newValue); @@ -156,7 +155,7 @@ public class MoneyGramForm extends PaymentMethodForm { else flowPane.setId("flow-pane-checkboxes-non-editable-bg"); - CurrencyUtil.getAllMoneyGramCurrencies().forEach(e -> + paymentAccount.getSupportedCurrencies().forEach(e -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, paymentAccount)); } @@ -172,11 +171,7 @@ public class MoneyGramForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { - accountNameTextField.setText(Res.get(paymentAccount.getPaymentMethod().getId()) - .concat(": ") - .concat(StringUtils.abbreviate(holderNameInputTextField.getText(), 9))); - } + setAccountNameWithString(moneyGramAccountPayload.getHolderName()); } @Override diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/NeftForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/NeftForm.java new file mode 100644 index 0000000000..59d2ded3b7 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/NeftForm.java @@ -0,0 +1,38 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.layout.GridPane; + +public class NeftForm extends IfscBankForm { + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + return IfscBankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + } + + public NeftForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/NequiForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/NequiForm.java new file mode 100644 index 0000000000..5024b2b600 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/NequiForm.java @@ -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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; +import bisq.core.payment.NequiAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.NequiAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; + +public class NequiForm extends PaymentMethodForm { + private final NequiAccount account; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.mobile"), + ((NequiAccountPayload) paymentAccountPayload).getMobileNr(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); + return gridRow; + } + + public NequiForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (NequiAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + // this payment method is only for Columbia/COP + account.setSingleTradeCurrency(account.getSupportedCurrencies().get(0)); + CountryUtil.findCountryByCode("CO").ifPresent(c -> account.setCountry(c)); + + gridRowFrom = gridRow + 1; + + InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.mobile")); + mobileNrInputTextField.setValidator(inputValidator); + mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setMobileNr(newValue.trim()); + updateFromInputs(); + }); + + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getMobileNr()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), + account.getMobileNr()).second; + field.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && inputValidator.validate(account.getMobileNr()).isValid); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaxumForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaxumForm.java new file mode 100644 index 0000000000..14f68bced2 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaxumForm.java @@ -0,0 +1,113 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.validation.EmailValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.payment.PaxumAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaxumAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; + +public class PaxumForm extends PaymentMethodForm { + private final PaxumAccount account; + private final EmailValidator validator = new EmailValidator(); + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email"), + ((PaxumAccountPayload) paymentAccountPayload).getEmail()); + return gridRow; + } + + public PaxumForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (PaxumAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); + emailInputTextField.setValidator(validator); + emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setEmail(newValue.trim()); + updateFromInputs(); + }); + + addCurrenciesGrid(true); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + private void addCurrenciesGrid(boolean isEditable) { + FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, + Res.get("payment.supportedCurrenciesForReceiver"), 20, 20).second; + + if (isEditable) { + flowPane.setId("flow-pane-checkboxes-bg"); + } else { + flowPane.setId("flow-pane-checkboxes-non-editable-bg"); + } + + paymentAccount.getSupportedCurrencies().forEach(currency -> + fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getEmail()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), + account.getEmail()).second; + field.setMouseTransparent(false); + addLimitations(true); + addCurrenciesGrid(false); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && account.getEmail() != null + && validator.validate(account.getEmail()).isValid + && account.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java index 352982fc41..588fb944a7 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java @@ -28,13 +28,12 @@ import bisq.desktop.util.Layout; import bisq.core.account.witness.AccountAgeWitness; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.locale.Country; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.FiatCurrency; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferDirection; import bisq.core.payment.AssetAccount; import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentMethod; @@ -68,11 +67,11 @@ import javafx.collections.FXCollections; import javafx.util.StringConverter; import java.util.Date; -import java.util.List; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; +import static bisq.desktop.util.DisplayUtils.createAccountName; import static bisq.desktop.util.FormBuilder.*; @Slf4j @@ -123,19 +122,21 @@ public abstract class PaymentMethodForm { } protected void addAccountNameTextFieldWithAutoFillToggleButton() { + boolean isEditMode = paymentAccount.getPersistedAccountName() != null; Tuple3 tuple = addTopLabelInputTextFieldSlideToggleButton(gridPane, ++gridRow, Res.get("payment.account.name"), Res.get("payment.useCustomAccountName")); accountNameTextField = tuple.second; accountNameTextField.setPrefWidth(300); - accountNameTextField.setEditable(false); + accountNameTextField.setEditable(isEditMode); accountNameTextField.setValidator(inputValidator); accountNameTextField.setFocusTraversable(false); + accountNameTextField.setText(paymentAccount.getAccountName()); accountNameTextField.textProperty().addListener((ov, oldValue, newValue) -> { paymentAccount.setAccountName(newValue); updateAllInputsValid(); }); useCustomAccountNameToggleButton = tuple.third; - useCustomAccountNameToggleButton.setSelected(false); + useCustomAccountNameToggleButton.setSelected(isEditMode); useCustomAccountNameToggleButton.setOnAction(e -> { boolean selected = useCustomAccountNameToggleButton.isSelected(); accountNameTextField.setEditable(selected); @@ -147,7 +148,7 @@ public abstract class PaymentMethodForm { public static InfoTextField addOpenTradeDuration(GridPane gridPane, int gridRow, Offer offer) { - long hours = offer.getMaxTradePeriod() / 3600_000; + long hours = offer.getPaymentMethod().getMaxTradePeriod() / 3600_000; final Tuple3 labelInfoTextFieldVBoxTuple3 = addTopLabelInfoTextField(gridPane, gridRow, Res.get("payment.maxPeriod"), getTimeText(hours), -Layout.FLOATING_LABEL_DISTANCE); @@ -187,14 +188,14 @@ public abstract class PaymentMethodForm { Res.get("payment.maxPeriodAndLimitCrypto", getTimeText(hours), formatter.formatCoinWithCode(Coin.valueOf(accountAgeWitnessService.getMyTradeLimit( - paymentAccount, tradeCurrency.getCode(), OfferPayload.Direction.BUY)))) + paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY)))) : Res.get("payment.maxPeriodAndLimit", getTimeText(hours), formatter.formatCoinWithCode(Coin.valueOf(accountAgeWitnessService.getMyTradeLimit( - paymentAccount, tradeCurrency.getCode(), OfferPayload.Direction.BUY))), + paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY))), formatter.formatCoinWithCode(Coin.valueOf(accountAgeWitnessService.getMyTradeLimit( - paymentAccount, tradeCurrency.getCode(), OfferPayload.Direction.SELL))), + paymentAccount, tradeCurrency.getCode(), OfferDirection.SELL))), DisplayUtils.formatAccountAge(accountAge)); return limitationsText; } @@ -287,10 +288,8 @@ public abstract class PaymentMethodForm { void setAccountNameWithString(String name) { if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { - name = name.trim(); - name = StringUtils.abbreviate(name, 9); - String method = Res.get(paymentAccount.getPaymentMethod().getId()); - accountNameTextField.setText(method.concat(": ").concat(name)); + String accountName = createAccountName(paymentAccount.getPaymentMethod().getId(), name); + accountNameTextField.setText(accountName); } } @@ -313,32 +312,11 @@ public abstract class PaymentMethodForm { flowPane.getChildren().add(checkBox); } - void fillUpFlowPaneWithCountries(List checkBoxList, FlowPane flowPane, Country country) { - final String countryCode = country.code; - CheckBox checkBox = new AutoTooltipCheckBox(countryCode); - checkBox.setUserData(countryCode); - checkBoxList.add(checkBox); - checkBox.setMouseTransparent(false); - checkBox.setMinWidth(45); - checkBox.setMaxWidth(45); - checkBox.setTooltip(new Tooltip(country.name)); - checkBox.setOnAction(event -> { - if (checkBox.isSelected()) { - addAcceptedCountry(countryCode); - } else { - removeAcceptedCountry(countryCode); - } - - updateAllInputsValid(); - }); - flowPane.getChildren().add(checkBox); - } - protected abstract void autoFillNameTextField(); public abstract void addFormForAddAccount(); - public abstract void addFormForDisplayAccount(); + public abstract void addFormForEditAccount(); protected abstract void updateAllInputsValid(); diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PayseraForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PayseraForm.java new file mode 100644 index 0000000000..ef4e829448 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PayseraForm.java @@ -0,0 +1,113 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.validation.EmailValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PayseraAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PayseraAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; + +public class PayseraForm extends PaymentMethodForm { + private final PayseraAccount account; + private final EmailValidator validator = new EmailValidator(); + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email"), + ((PayseraAccountPayload) paymentAccountPayload).getEmail()); + return gridRow; + } + + public PayseraForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (PayseraAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); + emailInputTextField.setValidator(validator); + emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setEmail(newValue.trim()); + updateFromInputs(); + }); + + addCurrenciesGrid(true); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + private void addCurrenciesGrid(boolean isEditable) { + FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, + Res.get("payment.supportedCurrenciesForReceiver"), 20, 20).second; + + if (isEditable) { + flowPane.setId("flow-pane-checkboxes-bg"); + } else { + flowPane.setId("flow-pane-checkboxes-non-editable-bg"); + } + + paymentAccount.getSupportedCurrencies().forEach(currency -> + fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getEmail()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), + account.getEmail()).second; + field.setMouseTransparent(false); + addLimitations(true); + addCurrenciesGrid(false); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && account.getEmail() != null + && validator.validate(account.getEmail()).isValid + && account.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaytmForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaytmForm.java new file mode 100644 index 0000000000..0da8b9dc0b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaytmForm.java @@ -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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaytmAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaytmAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; + +public class PaytmForm extends PaymentMethodForm { + private final PaytmAccount account; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.email.mobile"), + ((PaytmAccountPayload) paymentAccountPayload).getEmailOrMobileNr(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); + return gridRow; + } + + public PaytmForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (PaytmAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + // this payment method is only for India/INR + account.setSingleTradeCurrency(account.getSupportedCurrencies().get(0)); + CountryUtil.findCountryByCode("IN").ifPresent(c -> account.setCountry(c)); + + gridRowFrom = gridRow + 1; + + InputTextField emailOrMobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email.mobile")); + emailOrMobileNrInputTextField.setValidator(inputValidator); + emailOrMobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setEmailOrMobileNr(newValue.trim()); + updateFromInputs(); + }); + + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getEmailOrMobileNr()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile"), + account.getEmailOrMobileNr()).second; + field.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && inputValidator.validate(account.getEmailOrMobileNr()).isValid); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PixForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PixForm.java new file mode 100644 index 0000000000..5fe825bfe1 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PixForm.java @@ -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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PixAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PixAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; + +public class PixForm extends PaymentMethodForm { + private final PixAccount account; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.pix.key"), + ((PixAccountPayload) paymentAccountPayload).getPixKey(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); + return gridRow; + } + + public PixForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (PixAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + // this payment method is only for Brazil/BRL + account.setSingleTradeCurrency(account.getSupportedCurrencies().get(0)); + CountryUtil.findCountryByCode("BR").ifPresent(c -> account.setCountry(c)); + + gridRowFrom = gridRow + 1; + + InputTextField pixKeyInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.pix.key")); + pixKeyInputTextField.setValidator(inputValidator); + pixKeyInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setPixKey(newValue.trim()); + updateFromInputs(); + }); + + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getPixKey()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.pix.key"), + account.getPixKey()).second; + field.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && inputValidator.validate(account.getPixKey()).isValid); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PopmoneyForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PopmoneyForm.java index 59a99ddfd5..e69d0b9e83 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PopmoneyForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PopmoneyForm.java @@ -19,7 +19,6 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.PopmoneyValidator; import bisq.core.account.witness.AccountAgeWitnessService; @@ -42,7 +41,6 @@ import static bisq.desktop.util.FormBuilder.addTopLabelTextField; public class PopmoneyForm extends PaymentMethodForm { private final PopmoneyAccount account; private final PopmoneyValidator validator; - private InputTextField accountIdInputTextField; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), @@ -69,7 +67,7 @@ public class PopmoneyForm extends PaymentMethodForm { updateFromInputs(); }); - accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.popmoney.accountId")); + InputTextField accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.popmoney.accountId")); accountIdInputTextField.setValidator(validator); accountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setAccountId(newValue.trim()); @@ -85,13 +83,13 @@ public class PopmoneyForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(accountIdInputTextField.getText()); + setAccountNameWithString(account.getAccountId()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), account.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), account.getHolderName()); diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PromptPayForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PromptPayForm.java index bade8ac028..732e33dcb0 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PromptPayForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PromptPayForm.java @@ -18,7 +18,6 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.PromptPayValidator; import bisq.core.account.witness.AccountAgeWitnessService; @@ -41,7 +40,6 @@ import static bisq.desktop.util.FormBuilder.addTopLabelTextField; public class PromptPayForm extends PaymentMethodForm { private final PromptPayAccount promptPayAccount; private final PromptPayValidator promptPayValidator; - private InputTextField promptPayIdInputTextField; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { @@ -61,7 +59,7 @@ public class PromptPayForm extends PaymentMethodForm { public void addFormForAddAccount() { gridRowFrom = gridRow + 1; - promptPayIdInputTextField = addInputTextField(gridPane, ++gridRow, + InputTextField promptPayIdInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.promptPay.promptPayId")); promptPayIdInputTextField.setValidator(promptPayValidator); promptPayIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { @@ -78,14 +76,13 @@ public class PromptPayForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(promptPayIdInputTextField.getText()); + setAccountNameWithString(promptPayAccount.getPromptPayId()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - promptPayAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(promptPayAccount.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.promptPay.promptPayId"), diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java index 6f3141f833..97a0102c7f 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java @@ -23,7 +23,6 @@ import bisq.desktop.util.Layout; import bisq.desktop.util.validation.RevolutValidator; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.payment.PaymentAccount; import bisq.core.payment.RevolutAccount; @@ -43,13 +42,11 @@ import lombok.extern.slf4j.Slf4j; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static bisq.desktop.util.FormBuilder.addTopLabelFlowPane; -import static bisq.desktop.util.FormBuilder.addTopLabelTextField; @Slf4j public class RevolutForm extends PaymentMethodForm { private final RevolutAccount account; - private RevolutValidator validator; - private InputTextField userNameInputTextField; + private final RevolutValidator validator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { @@ -70,7 +67,7 @@ public class RevolutForm extends PaymentMethodForm { public void addFormForAddAccount() { gridRowFrom = gridRow + 1; - userNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.userName")); + InputTextField userNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.userName")); userNameInputTextField.setValidator(validator); userNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setUserName(newValue.trim()); @@ -92,20 +89,19 @@ public class RevolutForm extends PaymentMethodForm { else flowPane.setId("flow-pane-checkboxes-non-editable-bg"); - CurrencyUtil.getAllRevolutCurrencies().forEach(e -> + account.getSupportedCurrencies().forEach(e -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, account)); } @Override protected void autoFillNameTextField() { - setAccountNameWithString(userNameInputTextField.getText()); + setAccountNameWithString(account.getUserName()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - account.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/RtgsForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/RtgsForm.java new file mode 100644 index 0000000000..8d73718c33 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/RtgsForm.java @@ -0,0 +1,38 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.layout.GridPane; + +public class RtgsForm extends IfscBankForm { + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + return IfscBankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + } + + public RtgsForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, + GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/SatispayForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SatispayForm.java new file mode 100644 index 0000000000..680e0d9639 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SatispayForm.java @@ -0,0 +1,112 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.SatispayAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.SatispayAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class SatispayForm extends PaymentMethodForm { + private final SatispayAccount account; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 0, Res.get("payment.account.owner"), + ((SatispayAccountPayload) paymentAccountPayload).getHolderName()); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.mobile"), + ((SatispayAccountPayload) paymentAccountPayload).getMobileNr()); + return gridRow; + } + + public SatispayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (SatispayAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + // this payment method is only for Italy/EUR + account.setSingleTradeCurrency(account.getSupportedCurrencies().get(0)); + CountryUtil.findCountryByCode("IT").ifPresent(c -> account.setCountry(c)); + + gridRowFrom = gridRow + 1; + + InputTextField holderNameField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner")); + holderNameField.setValidator(inputValidator); + holderNameField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setHolderName(newValue.trim()); + updateFromInputs(); + }); + + InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.mobile")); + mobileNrInputTextField.setValidator(inputValidator); + mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setMobileNr(newValue.trim()); + updateFromInputs(); + }); + + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getMobileNr()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), account.getHolderName()) + .second.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), account.getMobileNr()) + .second.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && inputValidator.validate(account.getHolderName()).isValid + && inputValidator.validate(account.getMobileNr()).isValid); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/SepaForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SepaForm.java index f534f6b13f..5ce4a68fdd 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/SepaForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SepaForm.java @@ -19,15 +19,13 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; -import bisq.desktop.util.validation.BICValidator; -import bisq.desktop.util.validation.IBANValidator; import bisq.desktop.util.normalization.IBANNormalizer; +import bisq.desktop.util.validation.BICValidator; +import bisq.desktop.util.validation.SepaIBANValidator; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.Country; import bisq.core.locale.CountryUtil; -import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.payment.PaymentAccount; @@ -37,20 +35,17 @@ import bisq.core.payment.payload.SepaAccountPayload; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.InputValidator; -import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; -import javafx.scene.control.TextField; import javafx.scene.control.TextFormatter; -import javafx.scene.control.Tooltip; import javafx.scene.layout.GridPane; import javafx.collections.FXCollections; import java.util.List; +import java.util.Optional; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; -import static bisq.desktop.util.FormBuilder.addTopLabelTextField; public class SepaForm extends GeneralSepaForm { @@ -72,16 +67,20 @@ public class SepaForm extends GeneralSepaForm { } private final SepaAccount sepaAccount; - private final IBANValidator ibanValidator; + private final SepaIBANValidator sepaIBANValidator; private final BICValidator bicValidator; - public SepaForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, IBANValidator ibanValidator, - BICValidator bicValidator, InputValidator inputValidator, - GridPane gridPane, int gridRow, CoinFormatter formatter) { + public SepaForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, + BICValidator bicValidator, + InputValidator inputValidator, + GridPane gridPane, + int gridRow, + CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.sepaAccount = (SepaAccount) paymentAccount; - this.ibanValidator = ibanValidator; this.bicValidator = bicValidator; + this.sepaIBANValidator = new SepaIBANValidator(); } @Override @@ -96,14 +95,10 @@ public class SepaForm extends GeneralSepaForm { updateFromInputs(); }); - ibanInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, IBAN); + InputTextField ibanInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, IBAN); ibanInputTextField.setTextFormatter(new TextFormatter<>(new IBANNormalizer())); - ibanInputTextField.setValidator(ibanValidator); - ibanInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { - sepaAccount.setIban(newValue); - updateFromInputs(); + ibanInputTextField.setValidator(sepaIBANValidator); - }); InputTextField bicInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, BIC); bicInputTextField.setValidator(bicValidator); bicInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { @@ -116,8 +111,8 @@ public class SepaForm extends GeneralSepaForm { setCountryComboBoxAction(countryComboBox, sepaAccount); - addEuroCountriesGrid(); - addNonEuroCountriesGrid(); + addCountriesGrid(Res.get("payment.accept.euro"), CountryUtil.getAllSepaEuroCountries()); + addCountriesGrid(Res.get("payment.accept.nonEuro"), CountryUtil.getAllSepaNonEuroCountries()); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); @@ -128,47 +123,35 @@ public class SepaForm extends GeneralSepaForm { sepaAccount.setCountry(country); } - updateFromInputs(); - } + ibanInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + sepaAccount.setIban(newValue); + updateFromInputs(); + if (ibanInputTextField.validate()) { + List countries = CountryUtil.getAllSepaCountries(); + String ibanCountryCode = newValue.substring(0, 2).toUpperCase(); + Optional ibanCountry = countries + .stream() + .filter(c -> c.code.equals(ibanCountryCode)) + .findFirst(); - private void addEuroCountriesGrid() { - addCountriesGrid(Res.get("payment.accept.euro"), euroCountryCheckBoxes, - CountryUtil.getAllSepaEuroCountries()); - } - - private void addNonEuroCountriesGrid() { - addCountriesGrid(Res.get("payment.accept.nonEuro"), nonEuroCountryCheckBoxes, - CountryUtil.getAllSepaNonEuroCountries()); - } - - @Override - void updateCountriesSelection(List checkBoxList) { - checkBoxList.forEach(checkBox -> { - String countryCode = (String) checkBox.getUserData(); - TradeCurrency selectedCurrency = sepaAccount.getSelectedTradeCurrency(); - if (selectedCurrency == null) { - Country country = CountryUtil.getDefaultCountry(); - if (CountryUtil.getAllSepaCountries().contains(country)) - selectedCurrency = CurrencyUtil.getCurrencyByCountryCode(country.code); + ibanCountry.ifPresent(countryComboBox::setValue); } - - boolean selected; - if (selectedCurrency != null) { - selected = true; - sepaAccount.addAcceptedCountry(countryCode); - } else { - selected = sepaAccount.getAcceptedCountryCodes().contains(countryCode); - } - checkBox.setSelected(selected); }); + + countryComboBox.valueProperty().addListener((ov, oldValue, newValue) -> { + sepaIBANValidator.setRestrictToCountry(newValue.code); + ibanInputTextField.refreshValidation(); + }); + + updateFromInputs(); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && bicValidator.validate(sepaAccount.getBic()).isValid - && ibanValidator.validate(sepaAccount.getIban()).isValid + && sepaIBANValidator.validate(sepaAccount.getIban()).isValid && inputValidator.validate(sepaAccount.getHolderName()).isValid && sepaAccount.getAcceptedCountryCodes().size() > 0 && sepaAccount.getSingleTradeCurrency() != null @@ -176,9 +159,9 @@ public class SepaForm extends GeneralSepaForm { } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), sepaAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(sepaAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), sepaAccount.getHolderName()); @@ -189,19 +172,9 @@ public class SepaForm extends GeneralSepaForm { TradeCurrency singleTradeCurrency = sepaAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); - String countries; - Tooltip tooltip = null; - if (CountryUtil.containsAllSepaEuroCountries(sepaAccount.getAcceptedCountryCodes())) { - countries = Res.get("shared.allEuroCountries"); - } else { - countries = CountryUtil.getCodesString(sepaAccount.getAcceptedCountryCodes()); - tooltip = new Tooltip(CountryUtil.getNamesByCodesString(sepaAccount.getAcceptedCountryCodes())); - } - TextField acceptedCountries = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.accepted.countries"), countries).second; - if (tooltip != null) { - acceptedCountries.setMouseTransparent(false); - acceptedCountries.setTooltip(tooltip); - } + + addCountriesGrid(Res.get("payment.accept.euro"), CountryUtil.getAllSepaEuroCountries()); + addCountriesGrid(Res.get("payment.accept.nonEuro"), CountryUtil.getAllSepaNonEuroCountries()); addLimitations(true); } @@ -214,4 +187,14 @@ public class SepaForm extends GeneralSepaForm { void addAcceptedCountry(String countryCode) { sepaAccount.addAcceptedCountry(countryCode); } + + @Override + boolean isCountryAccepted(String countryCode) { + return sepaAccount.getAcceptedCountryCodes().contains(countryCode); + } + + @Override + protected String getIban() { + return sepaAccount.getIban(); + } } diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/SepaInstantForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SepaInstantForm.java index 719580d625..8d359c4af7 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/SepaInstantForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SepaInstantForm.java @@ -19,15 +19,13 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; -import bisq.desktop.util.validation.BICValidator; -import bisq.desktop.util.validation.IBANValidator; import bisq.desktop.util.normalization.IBANNormalizer; +import bisq.desktop.util.validation.BICValidator; +import bisq.desktop.util.validation.SepaIBANValidator; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.Country; import bisq.core.locale.CountryUtil; -import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.payment.PaymentAccount; @@ -37,20 +35,17 @@ import bisq.core.payment.payload.SepaInstantAccountPayload; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.InputValidator; -import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; -import javafx.scene.control.TextField; import javafx.scene.control.TextFormatter; -import javafx.scene.control.Tooltip; import javafx.scene.layout.GridPane; import javafx.collections.FXCollections; import java.util.List; +import java.util.Optional; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; -import static bisq.desktop.util.FormBuilder.addTopLabelTextField; public class SepaInstantForm extends GeneralSepaForm { @@ -72,15 +67,19 @@ public class SepaInstantForm extends GeneralSepaForm { } private final SepaInstantAccount sepaInstantAccount; - private final IBANValidator ibanValidator; + private final SepaIBANValidator sepaIBANValidator; private final BICValidator bicValidator; - public SepaInstantForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, IBANValidator ibanValidator, - BICValidator bicValidator, InputValidator inputValidator, - GridPane gridPane, int gridRow, CoinFormatter formatter) { + public SepaInstantForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, + BICValidator bicValidator, + InputValidator inputValidator, + GridPane gridPane, + int gridRow, + CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.sepaInstantAccount = (SepaInstantAccount) paymentAccount; - this.ibanValidator = ibanValidator; + this.sepaIBANValidator = new SepaIBANValidator(); this.bicValidator = bicValidator; } @@ -96,14 +95,10 @@ public class SepaInstantForm extends GeneralSepaForm { updateFromInputs(); }); - ibanInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, IBAN); + InputTextField ibanInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, IBAN); ibanInputTextField.setTextFormatter(new TextFormatter<>(new IBANNormalizer())); - ibanInputTextField.setValidator(ibanValidator); - ibanInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { - sepaInstantAccount.setIban(newValue); - updateFromInputs(); + ibanInputTextField.setValidator(sepaIBANValidator); - }); InputTextField bicInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, BIC); bicInputTextField.setValidator(bicValidator); bicInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { @@ -116,8 +111,8 @@ public class SepaInstantForm extends GeneralSepaForm { setCountryComboBoxAction(countryComboBox, sepaInstantAccount); - addEuroCountriesGrid(); - addNonEuroCountriesGrid(); + addCountriesGrid(Res.get("payment.accept.euro"), CountryUtil.getAllSepaEuroCountries()); + addCountriesGrid(Res.get("payment.accept.nonEuro"), CountryUtil.getAllSepaNonEuroCountries()); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); @@ -128,46 +123,37 @@ public class SepaInstantForm extends GeneralSepaForm { sepaInstantAccount.setCountry(country); } - updateFromInputs(); - } + ibanInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + sepaInstantAccount.setIban(newValue); + updateFromInputs(); - private void addEuroCountriesGrid() { - addCountriesGrid(Res.get("payment.accept.euro"), euroCountryCheckBoxes, - CountryUtil.getAllSepaInstantEuroCountries()); - } + if (ibanInputTextField.validate()) { + List countries = CountryUtil.getAllSepaCountries(); + String ibanCountryCode = newValue.substring(0, 2).toUpperCase(); + Optional ibanCountry = countries + .stream() + .filter(c -> c.code.equals(ibanCountryCode)) + .findFirst(); - private void addNonEuroCountriesGrid() { - addCountriesGrid(Res.get("payment.accept.nonEuro"), nonEuroCountryCheckBoxes, - CountryUtil.getAllSepaInstantNonEuroCountries()); - } - - @Override - void updateCountriesSelection(List checkBoxList) { - checkBoxList.forEach(checkBox -> { - String countryCode = (String) checkBox.getUserData(); - TradeCurrency selectedCurrency = sepaInstantAccount.getSelectedTradeCurrency(); - if (selectedCurrency == null) { - Country country = CountryUtil.getDefaultCountry(); - if (CountryUtil.getAllSepaInstantCountries().contains(country)) - selectedCurrency = CurrencyUtil.getCurrencyByCountryCode(country.code); + if (ibanCountry.isPresent()) { + countryComboBox.setValue(ibanCountry.get()); + } } - - boolean selected; - if (selectedCurrency != null) { - selected = true; - sepaInstantAccount.addAcceptedCountry(countryCode); - } else { - selected = sepaInstantAccount.getAcceptedCountryCodes().contains(countryCode); - } - checkBox.setSelected(selected); }); + + countryComboBox.valueProperty().addListener((ov, oldValue, newValue) -> { + sepaIBANValidator.setRestrictToCountry(newValue.code); + ibanInputTextField.refreshValidation(); + }); + + updateFromInputs(); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && bicValidator.validate(sepaInstantAccount.getBic()).isValid - && ibanValidator.validate(sepaInstantAccount.getIban()).isValid + && sepaIBANValidator.validate(sepaInstantAccount.getIban()).isValid && inputValidator.validate(sepaInstantAccount.getHolderName()).isValid && sepaInstantAccount.getAcceptedCountryCodes().size() > 0 && sepaInstantAccount.getSingleTradeCurrency() != null @@ -175,9 +161,9 @@ public class SepaInstantForm extends GeneralSepaForm { } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), sepaInstantAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(sepaInstantAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), sepaInstantAccount.getHolderName()); @@ -188,19 +174,9 @@ public class SepaInstantForm extends GeneralSepaForm { TradeCurrency singleTradeCurrency = sepaInstantAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); - String countries; - Tooltip tooltip = null; - if (CountryUtil.containsAllSepaInstantEuroCountries(sepaInstantAccount.getAcceptedCountryCodes())) { - countries = Res.get("shared.allEuroCountries"); - } else { - countries = CountryUtil.getCodesString(sepaInstantAccount.getAcceptedCountryCodes()); - tooltip = new Tooltip(CountryUtil.getNamesByCodesString(sepaInstantAccount.getAcceptedCountryCodes())); - } - TextField acceptedCountries = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.accepted.countries"), countries).second; - if (tooltip != null) { - acceptedCountries.setMouseTransparent(false); - acceptedCountries.setTooltip(tooltip); - } + + addCountriesGrid(Res.get("payment.accept.euro"), CountryUtil.getAllSepaEuroCountries()); + addCountriesGrid(Res.get("payment.accept.nonEuro"), CountryUtil.getAllSepaNonEuroCountries()); addLimitations(true); } @@ -213,4 +189,14 @@ public class SepaInstantForm extends GeneralSepaForm { void addAcceptedCountry(String countryCode) { sepaInstantAccount.addAcceptedCountry(countryCode); } + + @Override + boolean isCountryAccepted(String countryCode) { + return sepaInstantAccount.getAcceptedCountryCodes().contains(countryCode); + } + + @Override + protected String getIban() { + return sepaInstantAccount.getIban(); + } } diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/StrikeForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/StrikeForm.java new file mode 100644 index 0000000000..2adc0a1160 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/StrikeForm.java @@ -0,0 +1,102 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.StrikeAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.StrikeAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; + +public class StrikeForm extends PaymentMethodForm { + private final StrikeAccount account; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.account.userName"), + ((StrikeAccountPayload) paymentAccountPayload).getHolderName(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); + return gridRow; + } + + public StrikeForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (StrikeAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + // this payment method is currently restricted to United States/USD + CountryUtil.findCountryByCode("US").ifPresent(account::setCountry); + + gridRowFrom = gridRow + 1; + + InputTextField holderNameField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.userName")); + holderNameField.setValidator(inputValidator); + holderNameField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setHolderName(newValue.trim()); + updateFromInputs(); + }); + + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getHolderName()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.userName"), + account.getHolderName()).second; + field.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && inputValidator.validate(account.getHolderName()).isValid); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/SwiftForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SwiftForm.java new file mode 100644 index 0000000000..b559412249 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SwiftForm.java @@ -0,0 +1,345 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipCheckBox; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.windows.SwiftPaymentDetails; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.LengthValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.SwiftAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.SwiftAccountPayload; +import bisq.core.trade.Trade; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import com.jfoenix.controls.JFXTextArea; + +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.control.TitledPane; +import javafx.scene.layout.GridPane; + +import javafx.geometry.Insets; + +import java.util.function.Consumer; + +import static bisq.common.util.Utilities.cleanString; +import static bisq.core.payment.payload.SwiftAccountPayload.*; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addInputTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextArea; + +public class SwiftForm extends PaymentMethodForm { + private final SwiftAccountPayload formData; + private final AutoTooltipCheckBox useIntermediaryCheck; + private final LengthValidator defaultValidator = new LengthValidator(2, 34); + private final LengthValidator swiftValidator = new LengthValidator(11, 11); + private final LengthValidator accountNrValidator = new LengthValidator(2, 40); + private final LengthValidator addressValidator = new LengthValidator(1, 100); + + public SwiftForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, + InputValidator defaultValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, defaultValidator, gridPane, gridRow, formatter); + this.formData = ((SwiftAccount) paymentAccount).getPayload(); + this.useIntermediaryCheck = new AutoTooltipCheckBox(Res.get("payment.swift.use.intermediary")); + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + addFieldsForBankEdit(true, this::setBankSwiftCode, this::setBankName, this::setBankBranch, this::setBankAddress); + addFieldsForBankEdit(false, this::setIntermediarySwiftCode, this::setIntermediaryName, this::setIntermediaryBranch, this::setIntermediaryAddress); + addFieldsForBeneficiaryEdit(); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(formData.getBeneficiaryName()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); + + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(SWIFT_CODE + BANKPOSTFIX), formData.getBankSwiftCode()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(COUNTRY + BANKPOSTFIX), CountryUtil.getNameAndCode(formData.getBankCountryCode())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(SNAME + BANKPOSTFIX), formData.getBankName()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(BRANCH + BANKPOSTFIX), formData.getBankBranch()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(ADDRESS + BANKPOSTFIX), cleanString(formData.getBankAddress())); + + if (formData.usesIntermediaryBank()) { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(SWIFT_CODE + INTERMEDIARYPOSTFIX), formData.getIntermediarySwiftCode(), Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(COUNTRY + INTERMEDIARYPOSTFIX), CountryUtil.getNameAndCode(formData.getIntermediaryCountryCode())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(SNAME + INTERMEDIARYPOSTFIX), formData.getIntermediaryName()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(BRANCH + INTERMEDIARYPOSTFIX), formData.getIntermediaryBranch()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(ADDRESS + INTERMEDIARYPOSTFIX), cleanString(formData.getIntermediaryAddress())); + } + + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), formData.getBeneficiaryName(), Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(SWIFT_ACCOUNT), formData.getBeneficiaryAccountNr()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(ADDRESS + BENEFICIARYPOSTFIX), cleanString(formData.getBeneficiaryAddress())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(PHONE + BENEFICIARYPOSTFIX), formData.getBeneficiaryPhone()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.city"), formData.getBeneficiaryCity()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), CountryUtil.getNameAndCode(formData.getBankCountryCode())); // same as receiving bank country + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), cleanString(formData.getSpecialInstructions())); + + gridPane.add(new Label(""), 0, ++gridRow); // spacer + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + SwiftAccountPayload data = formData; + + // intermediary bank details are optional, but if specified must be valid + boolean intermediaryValidIfSpecified = !useIntermediaryCheck.isSelected() && !data.usesIntermediaryBank() || + data.usesIntermediaryBank() && (swiftValidator.validate(data.getIntermediarySwiftCode()).isValid + && defaultValidator.validate(data.getIntermediaryCountryCode()).isValid + && defaultValidator.validate(data.getIntermediaryName()).isValid + && defaultValidator.validate(data.getIntermediaryBranch()).isValid + && addressValidator.validate(data.getIntermediaryAddress()).isValid + ); + + allInputsValid.set(isAccountNameValid() + && swiftValidator.validate(data.getBankSwiftCode()).isValid + && defaultValidator.validate(data.getBankCountryCode()).isValid + && defaultValidator.validate(data.getBankName()).isValid + && defaultValidator.validate(data.getBankBranch()).isValid + && addressValidator.validate(data.getBankAddress()).isValid + && defaultValidator.validate(data.getBeneficiaryName()).isValid + && accountNrValidator.validate(data.getBeneficiaryAccountNr()).isValid + && addressValidator.validate(data.getBeneficiaryAddress()).isValid + && defaultValidator.validate(data.getBeneficiaryPhone()).isValid + && defaultValidator.validate(data.getBeneficiaryCity()).isValid + && paymentAccount.getTradeCurrencies().size() > 0 + && intermediaryValidIfSpecified); + } + + // Here we need to show information to buyer so they can make the fiat payment, however there is only enough space + // on the trade screen for ~4 fields. + // Since SWIFT has an unusually large number of fields, it will be better to offer a button which will show + // the SWIFT information in a popup screen. + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload, Trade trade) { + SwiftAccountPayload swiftAccountPayload = (SwiftAccountPayload) paymentAccountPayload; + Button button = new AutoTooltipButton(Res.get("payment.swift.showPaymentInfo")); + GridPane.setRowIndex(button, gridRow); + GridPane.setColumnIndex(button, 1); + gridPane.getChildren().add(button); + GridPane.setMargin(button, new Insets(Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, Layout.FLOATING_LABEL_DISTANCE)); + button.setOnAction((e) -> new SwiftPaymentDetails(swiftAccountPayload, trade).show()); + return gridRow; + } + + private void addFieldsForBankEdit(boolean isPrimary, + Consumer onSwiftCodeSelected, + Consumer onNameSelected, + Consumer onBranchSelected, + Consumer onAddressSelected) { + GridPane gridPane2 = new GridPane(); + gridPane2.getColumnConstraints().add(gridPane.getColumnConstraints().get(0)); + TitledPane titledPane = new TitledPane(isPrimary ? Res.get("payment.swift.title" + BANKPOSTFIX) : Res.get("payment.swift.title" + INTERMEDIARYPOSTFIX), gridPane2); + titledPane.setExpanded(isPrimary); + gridPane.add(titledPane, 0, ++gridRow); + + int gridRow2 = 0; + if (!isPrimary) { + // secondary bank (optional) has a checkbox to specify if it is being used + gridPane2.add(useIntermediaryCheck, 0, ++gridRow2); + } + String label = isPrimary ? Res.get(SWIFT_CODE + BANKPOSTFIX) : Res.get(SWIFT_CODE + INTERMEDIARYPOSTFIX); + InputTextField bankSwiftCodeField = addInputTextField(gridPane2, ++gridRow2, label); + bankSwiftCodeField.setPromptText(label); + bankSwiftCodeField.setValidator(swiftValidator); + bankSwiftCodeField.textProperty().addListener((ov, oldValue, newValue) -> onSwiftCodeSelected.accept(newValue)); + + if (isPrimary) { + gridRow2 = GUIUtil.addRegionCountry(gridPane2, gridRow2, this::setBankCountry); + } else { + gridRow2 = GUIUtil.addRegionCountry(gridPane2, ++gridRow2, this::setIntermediaryCountry); + } + + label = isPrimary ? Res.get(SNAME + BANKPOSTFIX) : Res.get(SNAME + INTERMEDIARYPOSTFIX); + InputTextField bankNameField = addInputTextField(gridPane2, ++gridRow2, label); + bankNameField.setPromptText(label); + bankNameField.setValidator(defaultValidator); + bankNameField.textProperty().addListener((ov, oldValue, newValue) -> onNameSelected.accept(newValue)); + + label = isPrimary ? Res.get(BRANCH + BANKPOSTFIX) : Res.get(BRANCH + INTERMEDIARYPOSTFIX); + InputTextField bankBranchField = addInputTextField(gridPane2, ++gridRow2, label); + bankBranchField.setPromptText(label); + bankBranchField.setValidator(defaultValidator); + bankBranchField.textProperty().addListener((ov, oldValue, newValue) -> onBranchSelected.accept(newValue)); + + label = isPrimary ? Res.get(ADDRESS + BANKPOSTFIX) : Res.get(ADDRESS + INTERMEDIARYPOSTFIX); + TextArea bankAddressTextArea = addTopLabelTextArea(gridPane2, ++gridRow2, label, label).second; + bankAddressTextArea.setMinHeight(70); + bankAddressTextArea.textProperty().addListener((ov, oldValue, newValue) -> onAddressSelected.accept(newValue)); + + // intermediary bank can be enabled/disabled via checkbox + if (!isPrimary) { + useIntermediaryCheck.setOnAction((e) -> { + for (Node x : gridPane2.getChildren()) { + if (x == useIntermediaryCheck) + continue; + x.setDisable(!useIntermediaryCheck.isSelected()); + } + if (!useIntermediaryCheck.isSelected()) { + bankSwiftCodeField.setText(""); + bankNameField.setText(""); + bankBranchField.setText(""); + bankAddressTextArea.setText(""); + } + updateFromInputs(); + }); + // make the intermediary fields initially greyed out + for (Node x : gridPane2.getChildren()) { + if (x == useIntermediaryCheck) + continue; + x.setDisable(!useIntermediaryCheck.isSelected()); + } + } + } + + private void addFieldsForBeneficiaryEdit() { + String label = Res.get("payment.account.owner"); + InputTextField beneficiaryNameField = addInputTextField(gridPane, ++gridRow, label); + beneficiaryNameField.setPromptText(label); + beneficiaryNameField.setValidator(defaultValidator); + beneficiaryNameField.textProperty().addListener((ov, oldValue, newValue) -> { + formData.setBeneficiaryName(newValue.trim()); + updateFromInputs(); + }); + + label = Res.get(SWIFT_ACCOUNT); + InputTextField beneficiaryAccountNrField = addInputTextField(gridPane, ++gridRow, label); + beneficiaryAccountNrField.setPromptText(label); + beneficiaryAccountNrField.setValidator(defaultValidator); + beneficiaryAccountNrField.setValidator(accountNrValidator); + beneficiaryAccountNrField.textProperty().addListener((ov, oldValue, newValue) -> { + formData.setBeneficiaryAccountNr(newValue.trim()); + updateFromInputs(); + }); + + label = Res.get(ADDRESS + BENEFICIARYPOSTFIX); + TextArea beneficiaryAddressTextArea = addTopLabelTextArea(gridPane, ++gridRow, label, label).second; + beneficiaryAddressTextArea.setMinHeight(70); + beneficiaryAddressTextArea.textProperty().addListener((ov, oldValue, newValue) -> { + formData.setBeneficiaryAddress(newValue.trim()); + updateFromInputs(); + }); + + label = Res.get("payment.account.city"); + InputTextField beneficiaryCityField = addInputTextField(gridPane, ++gridRow, label); + beneficiaryCityField.setPromptText(label); + beneficiaryCityField.setValidator(defaultValidator); + beneficiaryCityField.textProperty().addListener((ov, oldValue, newValue) -> { + formData.setBeneficiaryCity(newValue.trim()); + updateFromInputs(); + }); + + label = Res.get(PHONE + BENEFICIARYPOSTFIX); + InputTextField beneficiaryPhoneField = addInputTextField(gridPane, ++gridRow, label); + beneficiaryPhoneField.setPromptText(label); + beneficiaryPhoneField.setValidator(defaultValidator); + beneficiaryPhoneField.textProperty().addListener((ov, oldValue, newValue) -> { + formData.setBeneficiaryPhone(newValue.trim()); + updateFromInputs(); + }); + + label = Res.get("payment.shared.optionalExtra"); + TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, label, label).second; + extraTextArea.setMinHeight(70); + ((JFXTextArea) extraTextArea).setLabelFloat(false); + extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { + formData.setSpecialInstructions(newValue.trim()); + updateFromInputs(); + }); + } + + private void setBankSwiftCode(String value) { + formData.setBankSwiftCode(value.trim()); + updateFromInputs(); + } + + private void setBankName(String value) { + formData.setBankName(value.trim()); + updateFromInputs(); + } + + private void setBankBranch(String value) { + formData.setBankBranch(value.trim()); + updateFromInputs(); + } + + private void setBankAddress(String value) { + formData.setBankAddress(value.trim()); + updateFromInputs(); + } + + private void setIntermediarySwiftCode(String value) { + formData.setIntermediarySwiftCode(value.trim()); + updateFromInputs(); + } + + private void setIntermediaryName(String value) { + formData.setIntermediaryName(value.trim()); + updateFromInputs(); + } + + private void setIntermediaryBranch(String value) { + formData.setIntermediaryBranch(value.trim()); + updateFromInputs(); + } + + private void setIntermediaryAddress(String value) { + formData.setIntermediaryAddress(value.trim()); + updateFromInputs(); + } + + private void setBankCountry(Country country) { + if (country == null) + return; + formData.setBankCountryCode(country.code); + updateFromInputs(); + } + + private void setIntermediaryCountry(Country country) { + if (country == null) + return; + formData.setIntermediaryCountryCode(country.code); + updateFromInputs(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/SwishForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SwishForm.java index ad1deae781..2777ea95d2 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/SwishForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/SwishForm.java @@ -19,7 +19,6 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.SwishValidator; import bisq.core.account.witness.AccountAgeWitnessService; @@ -45,7 +44,6 @@ import static bisq.desktop.util.FormBuilder.addTopLabelTextField; public class SwishForm extends PaymentMethodForm { private final SwishAccount swishAccount; private final SwishValidator swishValidator; - private InputTextField mobileNrInputTextField; public SwishForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, @@ -80,7 +78,7 @@ public class SwishForm extends PaymentMethodForm { updateFromInputs(); }); - mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.mobile")); mobileNrInputTextField.setValidator(swishValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { @@ -97,14 +95,13 @@ public class SwishForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(mobileNrInputTextField.getText()); + setAccountNameWithString(swishAccount.getMobileNr()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - swishAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(swishAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/TikkieForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/TikkieForm.java new file mode 100644 index 0000000000..abd9844476 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/TikkieForm.java @@ -0,0 +1,101 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.validation.IBANValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.TikkieAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.TikkieAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class TikkieForm extends PaymentMethodForm { + private final TikkieAccount account; + private final IBANValidator ibanValidator = new IBANValidator("NL"); + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 0, Res.get("payment.tikkie.iban"), + ((TikkieAccountPayload) paymentAccountPayload).getIban()); + return gridRow; + } + + public TikkieForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (TikkieAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + // this payment method is only for Netherlands/EUR + CountryUtil.findCountryByCode("NL").ifPresent(account::setCountry); + + gridRowFrom = gridRow + 1; + + InputTextField ibanField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.tikkie.iban")); + ibanField.setValidator(ibanValidator); + ibanField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setIban(newValue.trim()); + updateFromInputs(); + }); + + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getIban()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.tikkie.iban"), account.getIban()) + .second.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && ibanValidator.validate(account.getIban()).isValid); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/TransferwiseForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/TransferwiseForm.java index 44e86deb8f..0dcccd7238 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/TransferwiseForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/TransferwiseForm.java @@ -19,11 +19,9 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.TransferwiseValidator; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.payment.PaymentAccount; import bisq.core.payment.TransferwiseAccount; @@ -38,12 +36,10 @@ import javafx.scene.layout.GridPane; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; -import static bisq.desktop.util.FormBuilder.addTopLabelTextField; public class TransferwiseForm extends PaymentMethodForm { private final TransferwiseAccount account; - private TransferwiseValidator validator; - private InputTextField emailInputTextField; + private final TransferwiseValidator validator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { @@ -64,7 +60,7 @@ public class TransferwiseForm extends PaymentMethodForm { public void addFormForAddAccount() { gridRowFrom = gridRow + 1; - emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); + InputTextField emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); emailInputTextField.setValidator(validator); emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setEmail(newValue.trim()); @@ -86,20 +82,19 @@ public class TransferwiseForm extends PaymentMethodForm { flowPane.setId("flow-pane-checkboxes-non-editable-bg"); } - CurrencyUtil.getAllTransferwiseCurrencies().forEach(currency -> + account.getSupportedCurrencies().forEach(currency -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); } @Override protected void autoFillNameTextField() { - setAccountNameWithString(emailInputTextField.getText()); + setAccountNameWithString(account.getEmail()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - account.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/TransferwiseUsdForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/TransferwiseUsdForm.java new file mode 100644 index 0000000000..b1a5932705 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/TransferwiseUsdForm.java @@ -0,0 +1,136 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.EmailValidator; +import bisq.desktop.util.validation.LengthValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.TransferwiseUsdAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.TransferwiseUsdAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextArea; +import javafx.scene.layout.GridPane; + +import static bisq.common.util.Utilities.cleanString; +import static bisq.desktop.util.FormBuilder.*; + +public class TransferwiseUsdForm extends PaymentMethodForm { + private final TransferwiseUsdAccount account; + private final LengthValidator addressValidator = new LengthValidator(0, 100); + private final EmailValidator emailValidator = new EmailValidator(); + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + + addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.account.owner"), + ((TransferwiseUsdAccountPayload) paymentAccountPayload).getHolderName(), + Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); + + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 1, Res.get("payment.email"), + ((TransferwiseUsdAccountPayload) paymentAccountPayload).getEmail()); + + String address = ((TransferwiseUsdAccountPayload) paymentAccountPayload).getBeneficiaryAddress(); + if (address.length() > 0) { + TextArea textAddress = addCompactTopLabelTextArea(gridPane, gridRow, 0, Res.get("payment.account.address"), "").second; + textAddress.setMinHeight(70); + textAddress.setEditable(false); + textAddress.setText(address); + } + + return gridRow; + } + + public TransferwiseUsdForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (TransferwiseUsdAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + + CountryUtil.findCountryByCode("US").ifPresent(account::setCountry); + + gridRowFrom = gridRow + 1; + + InputTextField emailField = addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); + emailField.setValidator(emailValidator); + emailField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setEmail(newValue.trim()); + updateFromInputs(); + }); + + InputTextField holderNameField = addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner")); + holderNameField.setValidator(inputValidator); + holderNameField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setHolderName(newValue.trim()); + updateFromInputs(); + }); + + String addressLabel = Res.get("payment.account.owner.address") + Res.get("payment.transferwiseUsd.address"); + TextArea addressTextArea = addTopLabelTextArea(gridPane, ++gridRow, addressLabel, addressLabel).second; + addressTextArea.setMinHeight(70); + addressTextArea.textProperty().addListener((ov, oldValue, newValue) -> { + account.setBeneficiaryAddress(newValue.trim()); + updateFromInputs(); + }); + + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getHolderName()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), account.getEmail()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), account.getHolderName()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.address"), cleanString(account.getBeneficiaryAddress())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && emailValidator.validate(account.getEmail()).isValid + && inputValidator.validate(account.getHolderName()).isValid + && addressValidator.validate(account.getBeneficiaryAddress()).isValid + ); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/USPostalMoneyOrderForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/USPostalMoneyOrderForm.java index e4cae7e7d6..a95af184d6 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/USPostalMoneyOrderForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/USPostalMoneyOrderForm.java @@ -19,7 +19,6 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.USPostalMoneyOrderValidator; import bisq.core.account.witness.AccountAgeWitnessService; @@ -94,14 +93,13 @@ public class USPostalMoneyOrderForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(postalAddressTextArea.getText()); + setAccountNameWithString(usPostalMoneyOrderAccount.getPostalAddress()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - usPostalMoneyOrderAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(usPostalMoneyOrderAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), @@ -120,7 +118,7 @@ public class USPostalMoneyOrderForm extends PaymentMethodForm { public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && usPostalMoneyOrderValidator.validate(usPostalMoneyOrderAccount.getPostalAddress()).isValid - && !postalAddressTextArea.getText().isEmpty() + && !usPostalMoneyOrderAccount.getPostalAddress().isEmpty() && inputValidator.validate(usPostalMoneyOrderAccount.getHolderName()).isValid && usPostalMoneyOrderAccount.getTradeCurrencies().size() > 0); } diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/UpholdForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/UpholdForm.java index b38db995a6..9eaa7a8156 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/UpholdForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/UpholdForm.java @@ -19,11 +19,9 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.UpholdValidator; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.payment.PaymentAccount; import bisq.core.payment.UpholdAccount; @@ -38,17 +36,23 @@ import javafx.scene.layout.GridPane; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; -import static bisq.desktop.util.FormBuilder.addTopLabelTextField; public class UpholdForm extends PaymentMethodForm { private final UpholdAccount upholdAccount; - private UpholdValidator upholdValidator; - private InputTextField accountIdInputTextField; + private final UpholdValidator upholdValidator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + String accountOwner = ((UpholdAccountPayload) paymentAccountPayload).getAccountOwner(); + if (accountOwner.isEmpty()) { + accountOwner = Res.get("payment.ask"); + } + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.owner"), + accountOwner); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.uphold.accountId"), ((UpholdAccountPayload) paymentAccountPayload).getAccountId()); + return gridRow; } @@ -64,7 +68,15 @@ public class UpholdForm extends PaymentMethodForm { public void addFormForAddAccount() { gridRowFrom = gridRow + 1; - accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.uphold.accountId")); + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.account.owner")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + upholdAccount.setAccountOwner(newValue); + updateFromInputs(); + }); + + InputTextField accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.uphold.accountId")); accountIdInputTextField.setValidator(upholdValidator); accountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { upholdAccount.setAccountId(newValue.trim()); @@ -86,22 +98,23 @@ public class UpholdForm extends PaymentMethodForm { else flowPane.setId("flow-pane-checkboxes-non-editable-bg"); - CurrencyUtil.getAllUpholdCurrencies().forEach(e -> + paymentAccount.getSupportedCurrencies().forEach(e -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, upholdAccount)); } @Override protected void autoFillNameTextField() { - setAccountNameWithString(accountIdInputTextField.getText()); + setAccountNameWithString(upholdAccount.getAccountId()); } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), - upholdAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(upholdAccount.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + Res.get(upholdAccount.getAccountOwner())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.uphold.accountId"), upholdAccount.getAccountId()).second; field.setMouseTransparent(false); diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/UpiForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/UpiForm.java new file mode 100644 index 0000000000..50ef1a5829 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/UpiForm.java @@ -0,0 +1,102 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.UpiAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.UpiAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class UpiForm extends PaymentMethodForm { + private final UpiAccount account; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.upi.virtualPaymentAddress"), + ((UpiAccountPayload) paymentAccountPayload).getVirtualPaymentAddress()); + return gridRow; + } + + public UpiForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (UpiAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + // this payment method is only for India/INR + account.setSingleTradeCurrency(account.getSupportedCurrencies().get(0)); + CountryUtil.findCountryByCode("IN").ifPresent(c -> account.setCountry(c)); + + gridRowFrom = gridRow + 1; + + InputTextField virtualPaymentAddressInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.upi.virtualPaymentAddress")); + virtualPaymentAddressInputTextField.setValidator(inputValidator); + virtualPaymentAddressInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setVirtualPaymentAddress(newValue.trim()); + updateFromInputs(); + }); + + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getVirtualPaymentAddress()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.upi.virtualPaymentAddress"), + account.getVirtualPaymentAddress()).second; + field.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && inputValidator.validate(account.getVirtualPaymentAddress()).isValid); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/VerseForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/VerseForm.java new file mode 100644 index 0000000000..3d610275f9 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/VerseForm.java @@ -0,0 +1,111 @@ +/* + * 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 . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.VerseAccount; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.VerseAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; + +public class VerseForm extends PaymentMethodForm { + private final VerseAccount account; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.userName"), + ((VerseAccountPayload) paymentAccountPayload).getHolderName()); + return gridRow; + } + + public VerseForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (VerseAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.userName")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setHolderName(newValue.trim()); + updateFromInputs(); + }); + + addCurrenciesGrid(true); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + private void addCurrenciesGrid(boolean isEditable) { + FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, + Res.get("payment.supportedCurrencies"), 20, 20).second; + + if (isEditable) { + flowPane.setId("flow-pane-checkboxes-bg"); + } else { + flowPane.setId("flow-pane-checkboxes-non-editable-bg"); + } + + paymentAccount.getSupportedCurrencies().forEach(currency -> + fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getHolderName()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.userName"), + account.getHolderName()).second; + field.setMouseTransparent(false); + addLimitations(true); + addCurrenciesGrid(false); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && account.getHolderName() != null + && inputValidator.validate(account.getHolderName()).isValid + && account.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/WesternUnionForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/WesternUnionForm.java index d19a1c151e..4e6b2d141d 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/WesternUnionForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/WesternUnionForm.java @@ -20,7 +20,6 @@ package bisq.desktop.components.paymentmethods; import bisq.desktop.components.InputTextField; import bisq.desktop.util.FormBuilder; import bisq.desktop.util.GUIUtil; -import bisq.desktop.util.Layout; import bisq.desktop.util.validation.EmailValidator; import bisq.core.account.witness.AccountAgeWitnessService; @@ -39,8 +38,6 @@ import bisq.core.util.validation.InputValidator; import bisq.common.util.Tuple2; -import org.apache.commons.lang3.StringUtils; - import javafx.scene.control.ComboBox; import javafx.scene.layout.GridPane; @@ -48,7 +45,6 @@ import lombok.extern.slf4j.Slf4j; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; -import static bisq.desktop.util.FormBuilder.addTopLabelTextField; @Slf4j public class WesternUnionForm extends PaymentMethodForm { @@ -69,7 +65,6 @@ public class WesternUnionForm extends PaymentMethodForm { } private final WesternUnionAccountPayload westernUnionAccountPayload; - private InputTextField holderNameInputTextField; private InputTextField cityInputTextField; private InputTextField stateInputTextField; private final EmailValidator emailValidator; @@ -84,10 +79,10 @@ public class WesternUnionForm extends PaymentMethodForm { } @Override - public void addFormForDisplayAccount() { + public void addFormForEditAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), paymentAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), @@ -137,7 +132,7 @@ public class WesternUnionForm extends PaymentMethodForm { currencyComboBox = tuple.first; gridRow = tuple.second; - holderNameInputTextField = FormBuilder.addInputTextField(gridPane, + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.fullName")); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { westernUnionAccountPayload.setHolderName(newValue); @@ -185,11 +180,7 @@ public class WesternUnionForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { - accountNameTextField.setText(Res.get(paymentAccount.getPaymentMethod().getId()) - .concat(": ") - .concat(StringUtils.abbreviate(holderNameInputTextField.getText(), 9))); - } + setAccountNameWithString(westernUnionAccountPayload.getHolderName()); } @Override diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/data/JapanBankData.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/data/JapanBankData.java index d51ff527e7..9787bfae1c 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/data/JapanBankData.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/data/JapanBankData.java @@ -17,14 +17,13 @@ package bisq.desktop.components.paymentmethods.data; +import bisq.desktop.util.GUIUtil; + +import com.google.common.collect.ImmutableMap; + import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.HashMap; -import com.google.common.collect.ImmutableMap; - -import bisq.core.locale.Country; -import bisq.desktop.util.GUIUtil; /* Japan's National Banking Association assigns 4 digit codes to all @@ -46,8 +45,7 @@ import bisq.desktop.util.GUIUtil; Excel sheet: 金融機関等コード一覧 */ -public class JapanBankData -{ +public class JapanBankData { /* Returns the main list of ~500 banks in Japan with bank codes, but since 90%+ of people will be using one of ~30 major banks, @@ -56,11 +54,10 @@ public class JapanBankData */ public static List prettyPrintBankList() // {{{ { - List prettyList = new ArrayList(); + List prettyList = new ArrayList<>(); // add mega banks at the top - for (Map.Entry bank: megaBanksEnglish.entrySet()) - { + for (Map.Entry bank : megaBanksEnglish.entrySet()) { String bankId = bank.getKey(); String bankNameEn = bank.getValue(); String bankNameJa = majorBanksJapanese.get(bankId); @@ -69,8 +66,7 @@ public class JapanBankData } // append the major banks next - for (Map.Entry bank : majorBanksJapanese.entrySet()) - { + for (Map.Entry bank : majorBanksJapanese.entrySet()) { String bankId = bank.getKey(); String bankNameJa = bank.getValue(); // avoid duplicates @@ -79,8 +75,7 @@ public class JapanBankData } // append the minor local banks last - for (Map.Entry bank : minorBanksJapanese.entrySet()) - { + for (Map.Entry bank : minorBanksJapanese.entrySet()) { String bankId = bank.getKey(); String bankNameJa = bank.getValue(); prettyList.add(prettyPrintBank(bankId, bankNameJa)); @@ -93,690 +88,691 @@ public class JapanBankData private static String prettyPrintMajorBank(String bankId, String bankNameJa, String bankNameEn) // {{{ { return ID_OPEN + bankId + ID_CLOSE + SPACE + - JA_OPEN + bankNameJa + JA_CLOSE + SPACE + - EN_OPEN + bankNameEn + EN_CLOSE; + JA_OPEN + bankNameJa + JA_CLOSE + SPACE + + EN_OPEN + bankNameEn + EN_CLOSE; } // }}} + // Pretty print other banks like this: (9524) みずほ証券 private static String prettyPrintBank(String bankId, String bankName) // {{{ { return ID_OPEN + bankId + ID_CLOSE + SPACE + - JA_OPEN + bankName + JA_CLOSE; + JA_OPEN + bankName + JA_CLOSE; } // }}} // top 30 mega banks with english - private static final Map megaBanksEnglish = ImmutableMap. builder() - // {{{ japan post office - .put("9900", "Japan Post Bank Yucho") - // }}} - // {{{ japan mega-banks - .put("0001", "Mizuho Bank") - .put("0005", "Mitsubishi UFJ Bank (MUFG)") - .put("0009", "Sumitomo Mitsui Banking Corporation (SMBC)") - .put("0010", "Resona Bank") - // }}} - // {{{ major online banks - .put("0033", "Japan Net Bank") - .put("0034", "Seven Bank (7-11)") - .put("0035", "Sony Bank") - .put("0036", "Rakuten Bank") - .put("0038", "SBI Sumishin Net Bank") - .put("0039", "Jibun Bank") - .put("0040", "Aeon Bank") - .put("0042", "Lawson Bank") - // }}} - // {{{ major trust banks, etc. - .put("0150", "Suruga Bank") - .put("0288", "Mitsubishi UFJ Trust Bank") - .put("0289", "Mizuho Trust Bank") - .put("0294", "Sumitomo Trust Bank") - .put("0300", "SMBC Trust Bank (PRESTIA)") - .put("0304", "Nomura Trust Bank") - .put("0307", "Orix Trust Bank") - .put("0310", "GMO Aozora Net Bank") - .put("0321", "Japan Securities Trust Bank") - .put("0397", "Shinsei Bank") - .put("0398", "Aozora Bank") - .put("0402", "JP Morgan Chase Bank") - .put("0442", "BNY Mellon") - .put("0458", "DBS Bank") - .put("0472", "SBJ Shinhan Bank Japan") - // }}} - .build(); + private static final Map megaBanksEnglish = ImmutableMap.builder() + // {{{ japan post office + .put("9900", "Japan Post Bank Yucho") + // }}} + // {{{ japan mega-banks + .put("0001", "Mizuho Bank") + .put("0005", "Mitsubishi UFJ Bank (MUFG)") + .put("0009", "Sumitomo Mitsui Banking Corporation (SMBC)") + .put("0010", "Resona Bank") + // }}} + // {{{ major online banks + .put("0033", "Japan Net Bank") + .put("0034", "Seven Bank (7-11)") + .put("0035", "Sony Bank") + .put("0036", "Rakuten Bank") + .put("0038", "SBI Sumishin Net Bank") + .put("0039", "Jibun Bank") + .put("0040", "Aeon Bank") + .put("0042", "Lawson Bank") + // }}} + // {{{ major trust banks, etc. + .put("0150", "Suruga Bank") + .put("0288", "Mitsubishi UFJ Trust Bank") + .put("0289", "Mizuho Trust Bank") + .put("0294", "Sumitomo Trust Bank") + .put("0300", "SMBC Trust Bank (PRESTIA)") + .put("0304", "Nomura Trust Bank") + .put("0307", "Orix Trust Bank") + .put("0310", "GMO Aozora Net Bank") + .put("0321", "Japan Securities Trust Bank") + .put("0397", "Shinsei Bank") + .put("0398", "Aozora Bank") + .put("0402", "JP Morgan Chase Bank") + .put("0442", "BNY Mellon") + .put("0458", "DBS Bank") + .put("0472", "SBJ Shinhan Bank Japan") + // }}} + .build(); // major ~200 banks - private static final Map majorBanksJapanese = ImmutableMap. builder() - // {{{ ゆうちょ銀行 (9900) - .put("9900", "ゆうちょ銀行") - // }}} - // {{{ 都市銀行 (0001 ~ 0029) - .put("0001", "みずほ銀行") - .put("0005", "三菱UFJ銀行") - .put("0009", "三井住友銀行") - .put("0010", "りそな銀行") - .put("0017", "埼玉りそな銀行") - // }}} - // {{{ ネット専業銀行等 (0030 ~ 0049) - .put("0033", "ジャパンネット銀行") - .put("0034", "セブン銀行") - .put("0035", "ソニー銀行") - .put("0036", "楽天銀行") - .put("0038", "住信SBIネット銀行") - .put("0039", "じぶん銀行") - .put("0040", "イオン銀行") - .put("0041", "大和ネクスト銀行") - .put("0042", "ローソン銀行") - // }}} - // {{{ 協会 (0050 ~ 0099) - .put("0051", "全銀協") - .put("0052", "横浜銀行協会") - .put("0053", "釧路銀行協会") - .put("0054", "札幌銀行協会") - .put("0056", "函館銀行協会") - .put("0057", "青森銀行協会") - .put("0058", "秋田銀行協会") - .put("0059", "宮城銀行協会") - .put("0060", "福島銀行協会") - .put("0061", "群馬銀行協会") - .put("0062", "新潟銀行協会") - .put("0063", "石川銀行協会") - .put("0064", "山梨銀行協会") - .put("0065", "長野銀行協会") - .put("0066", "静岡銀行協会") - .put("0067", "名古屋銀行協会") - .put("0068", "京都銀行協会") - .put("0069", "大阪銀行協会") - .put("0070", "神戸銀行協会") - .put("0071", "岡山銀行協会") - .put("0072", "広島銀行協会") - .put("0073", "島根銀行協会") - .put("0074", "山口銀行協会") - .put("0075", "香川銀行協会") - .put("0076", "愛媛銀行協会") - .put("0077", "高知銀行協会") - .put("0078", "北九州銀行協会") - .put("0079", "福岡銀行協会") - .put("0080", "大分銀行協会") - .put("0081", "長崎銀行協会") - .put("0082", "熊本銀行協会") - .put("0083", "鹿児島銀行協会") - .put("0084", "沖縄銀行協会") - .put("0090", "全銀ネット") - .put("0095", "CLSBANK") - // }}} - // {{{ 地方銀行 (0116 ~ 0190) - .put("0116", "北海道銀行") - .put("0117", "青森銀行") - .put("0118", "みちのく銀行") - .put("0119", "秋田銀行") - .put("0120", "北都銀行") - .put("0121", "荘内銀行") - .put("0122", "山形銀行") - .put("0123", "岩手銀行") - .put("0124", "東北銀行") - .put("0125", "七十七銀行") - .put("0126", "東邦銀行") - .put("0128", "群馬銀行") - .put("0129", "足利銀行") - .put("0130", "常陽銀行") - .put("0131", "筑波銀行") - .put("0133", "武蔵野銀行") - .put("0134", "千葉銀行") - .put("0135", "千葉興業銀行") - .put("0137", "きらぼし銀行") - .put("0138", "横浜銀行") - .put("0140", "第四銀行") - .put("0141", "北越銀行") - .put("0142", "山梨中央銀行") - .put("0143", "八十二銀行") - .put("0144", "北陸銀行") - .put("0145", "富山銀行") - .put("0146", "北國銀行") - .put("0147", "福井銀行") - .put("0149", "静岡銀行") - .put("0150", "スルガ銀行") - .put("0151", "清水銀行") - .put("0152", "大垣共立銀行") - .put("0153", "十六銀行") - .put("0154", "三重銀行") - .put("0155", "百五銀行") - .put("0157", "滋賀銀行") - .put("0158", "京都銀行") - .put("0159", "関西みらい銀行") - .put("0161", "池田泉州銀行") - .put("0162", "南都銀行") - .put("0163", "紀陽銀行") - .put("0164", "但馬銀行") - .put("0166", "鳥取銀行") - .put("0167", "山陰合同銀行") - .put("0168", "中国銀行") - .put("0169", "広島銀行") - .put("0170", "山口銀行") - .put("0172", "阿波銀行") - .put("0173", "百十四銀行") - .put("0174", "伊予銀行") - .put("0175", "四国銀行") - .put("0177", "福岡銀行") - .put("0178", "筑邦銀行") - .put("0179", "佐賀銀行") - .put("0180", "十八銀行") - .put("0181", "親和銀行") - .put("0182", "肥後銀行") - .put("0183", "大分銀行") - .put("0184", "宮崎銀行") - .put("0185", "鹿児島銀行") - .put("0187", "琉球銀行") - .put("0188", "沖縄銀行") - .put("0190", "西日本シティ銀行") - .put("0191", "北九州銀行") - // }}} - // {{{ 信託銀行 (0288 ~ 0326) - .put("0288", "三菱UFJ信託銀行") - .put("0289", "みずほ信託銀行") - .put("0294", "三井住友信託銀行") - .put("0295", "BNYM信託") - .put("0297", "日本マスタートラスト信託銀行") - .put("0299", "ステート信託") - .put("0300", "SMBC信託銀行 プレスティア") - .put("0304", "野村信託銀行") - .put("0307", "オリックス銀行") - .put("0310", "GMOあおぞらネット銀行") - .put("0311", "農中信託") - .put("0320", "新生信託") - .put("0321", "日証金信託") - .put("0324", "日本トラスティサービス信託銀行") - .put("0325", "資産管理サービス信託銀行") - // }}} - // {{{ 旧長期信用銀行 (0397 ~ 0398) - .put("0397", "新生銀行") - .put("0398", "あおぞら銀行") - // }}} - // {{{ foreign banks (0400 ~ 0497) - .put("0401", "シティバンク、エヌ・エイ 銀行") - .put("0402", "JPモルガン・チェース銀行") - .put("0403", "アメリカ銀行") - .put("0411", "香港上海銀行") - .put("0413", "スタンチヤート") - .put("0414", "バークレイズ") - .put("0421", "アグリコル") - .put("0423", "ハナ") - .put("0424", "印度") - .put("0425", "兆豐國際商銀") - .put("0426", "バンコツク") - .put("0429", "バンクネガラ") - .put("0430", "ドイツ銀行") - .put("0432", "ブラジル") - .put("0438", "ユーオバシーズ") - .put("0439", "ユービーエス") - .put("0442", "BNYメロン") - .put("0443", "ビー・エヌ・ピー・パリバ銀行") - .put("0444", "チヤイニーズ") - .put("0445", "ソシエテ") - .put("0456", "ユバフ") - .put("0458", "DBS") - .put("0459", "パキスタン") - .put("0460", "クレデイスイス") - .put("0461", "コメルツ銀行") - .put("0463", "ウニクレデイト") - .put("0468", "インドステイト") - .put("0471", "カナダロイヤル") - .put("0472", "SBJ銀行") - .put("0477", "ウリイ") - .put("0482", "アイエヌジー") - .put("0484", "ナツトオース") - .put("0485", "アンズバンク") - .put("0487", "コモンウエルス") - .put("0489", "バンクチヤイナ") - .put("0495", "ステストリート") - .put("0498", "中小企業") - // }}} - // {{{ 第二地方銀行 (0501 ~ 0597) - .put("0501", "北洋銀行") - .put("0508", "きらやか銀行") - .put("0509", "北日本銀行") - .put("0512", "仙台銀行") - .put("0513", "福島銀行") - .put("0514", "大東銀行") - .put("0516", "東和銀行") - .put("0517", "栃木銀行") - .put("0522", "京葉銀行") - .put("0525", "東日本銀行") - .put("0526", "東京スター銀行") - .put("0530", "神奈川銀行") - .put("0532", "大光銀行") - .put("0533", "長野銀行") - .put("0534", "富山第一銀行") - .put("0537", "福邦銀行") - .put("0538", "静岡中央銀行") - .put("0542", "愛知銀行") - .put("0543", "名古屋銀行") - .put("0544", "中京銀行") - .put("0546", "第三銀行") - .put("0555", "大正銀行") - .put("0562", "みなと銀行") - .put("0565", "島根銀行") - .put("0566", "トマト銀行") - .put("0569", "もみじ銀行") - .put("0570", "西京銀行") - .put("0572", "徳島銀行") - .put("0573", "香川銀行") - .put("0576", "愛媛銀行") - .put("0578", "高知銀行") - .put("0582", "福岡中央銀行") - .put("0583", "佐賀共栄銀行") - .put("0585", "長崎銀行") - .put("0587", "熊本銀行") - .put("0590", "豊和銀行") - .put("0591", "宮崎太陽銀行") - .put("0594", "南日本銀行") - .put("0596", "沖縄海邦銀行") - // }}} - // {{{ more foreign banks (0600 ~ 0999) - .put("0603", "韓国産業") - .put("0607", "彰化商業") - .put("0608", "ウエルズフアゴ") - .put("0611", "第一商業") - .put("0612", "台湾") - .put("0615", "交通") - .put("0616", "メトロポリタン") - .put("0617", "フイリピン") - .put("0619", "中国工商") - .put("0621", "中國信託商業") - .put("0623", "インテーザ") - .put("0624", "國民") - .put("0625", "中国建設") - .put("0626", "イタウウニ") - .put("0627", "BBVA") - .put("0630", "中国農業") - .put("0631", "台新") - .put("0632", "玉山") - .put("0633", "台湾企銀") - .put("0808", "ドイツ証券") - .put("0813", "ソシエテ証券") - .put("0821", "ビーピー証券") - .put("0822", "バークレイ証券") - .put("0831", "アグリコル証券") - .put("0832", "ジエイピー証券") - .put("0842", "ゴルドマン証券") - .put("0845", "ナツトウエ証券") - .put("0900", "日本相互証券") - .put("0905", "東京金融取引所") - .put("0909", "日本クリア機構") - .put("0910", "ほふりクリア") - .put("0964", "しんきん証券") - .put("0966", "HSBC証券") - .put("0968", "セント東短証券") - .put("0971", "UBS証券") - .put("0972", "メリル日本証券") - // }}} - .build(); + private static final Map majorBanksJapanese = ImmutableMap.builder() + // {{{ ゆうちょ銀行 (9900) + .put("9900", "ゆうちょ銀行") + // }}} + // {{{ 都市銀行 (0001 ~ 0029) + .put("0001", "みずほ銀行") + .put("0005", "三菱UFJ銀行") + .put("0009", "三井住友銀行") + .put("0010", "りそな銀行") + .put("0017", "埼玉りそな銀行") + // }}} + // {{{ ネット専業銀行等 (0030 ~ 0049) + .put("0033", "ジャパンネット銀行") + .put("0034", "セブン銀行") + .put("0035", "ソニー銀行") + .put("0036", "楽天銀行") + .put("0038", "住信SBIネット銀行") + .put("0039", "じぶん銀行") + .put("0040", "イオン銀行") + .put("0041", "大和ネクスト銀行") + .put("0042", "ローソン銀行") + // }}} + // {{{ 協会 (0050 ~ 0099) + .put("0051", "全銀協") + .put("0052", "横浜銀行協会") + .put("0053", "釧路銀行協会") + .put("0054", "札幌銀行協会") + .put("0056", "函館銀行協会") + .put("0057", "青森銀行協会") + .put("0058", "秋田銀行協会") + .put("0059", "宮城銀行協会") + .put("0060", "福島銀行協会") + .put("0061", "群馬銀行協会") + .put("0062", "新潟銀行協会") + .put("0063", "石川銀行協会") + .put("0064", "山梨銀行協会") + .put("0065", "長野銀行協会") + .put("0066", "静岡銀行協会") + .put("0067", "名古屋銀行協会") + .put("0068", "京都銀行協会") + .put("0069", "大阪銀行協会") + .put("0070", "神戸銀行協会") + .put("0071", "岡山銀行協会") + .put("0072", "広島銀行協会") + .put("0073", "島根銀行協会") + .put("0074", "山口銀行協会") + .put("0075", "香川銀行協会") + .put("0076", "愛媛銀行協会") + .put("0077", "高知銀行協会") + .put("0078", "北九州銀行協会") + .put("0079", "福岡銀行協会") + .put("0080", "大分銀行協会") + .put("0081", "長崎銀行協会") + .put("0082", "熊本銀行協会") + .put("0083", "鹿児島銀行協会") + .put("0084", "沖縄銀行協会") + .put("0090", "全銀ネット") + .put("0095", "CLSBANK") + // }}} + // {{{ 地方銀行 (0116 ~ 0190) + .put("0116", "北海道銀行") + .put("0117", "青森銀行") + .put("0118", "みちのく銀行") + .put("0119", "秋田銀行") + .put("0120", "北都銀行") + .put("0121", "荘内銀行") + .put("0122", "山形銀行") + .put("0123", "岩手銀行") + .put("0124", "東北銀行") + .put("0125", "七十七銀行") + .put("0126", "東邦銀行") + .put("0128", "群馬銀行") + .put("0129", "足利銀行") + .put("0130", "常陽銀行") + .put("0131", "筑波銀行") + .put("0133", "武蔵野銀行") + .put("0134", "千葉銀行") + .put("0135", "千葉興業銀行") + .put("0137", "きらぼし銀行") + .put("0138", "横浜銀行") + .put("0140", "第四銀行") + .put("0141", "北越銀行") + .put("0142", "山梨中央銀行") + .put("0143", "八十二銀行") + .put("0144", "北陸銀行") + .put("0145", "富山銀行") + .put("0146", "北國銀行") + .put("0147", "福井銀行") + .put("0149", "静岡銀行") + .put("0150", "スルガ銀行") + .put("0151", "清水銀行") + .put("0152", "大垣共立銀行") + .put("0153", "十六銀行") + .put("0154", "三重銀行") + .put("0155", "百五銀行") + .put("0157", "滋賀銀行") + .put("0158", "京都銀行") + .put("0159", "関西みらい銀行") + .put("0161", "池田泉州銀行") + .put("0162", "南都銀行") + .put("0163", "紀陽銀行") + .put("0164", "但馬銀行") + .put("0166", "鳥取銀行") + .put("0167", "山陰合同銀行") + .put("0168", "中国銀行") + .put("0169", "広島銀行") + .put("0170", "山口銀行") + .put("0172", "阿波銀行") + .put("0173", "百十四銀行") + .put("0174", "伊予銀行") + .put("0175", "四国銀行") + .put("0177", "福岡銀行") + .put("0178", "筑邦銀行") + .put("0179", "佐賀銀行") + .put("0180", "十八銀行") + .put("0181", "親和銀行") + .put("0182", "肥後銀行") + .put("0183", "大分銀行") + .put("0184", "宮崎銀行") + .put("0185", "鹿児島銀行") + .put("0187", "琉球銀行") + .put("0188", "沖縄銀行") + .put("0190", "西日本シティ銀行") + .put("0191", "北九州銀行") + // }}} + // {{{ 信託銀行 (0288 ~ 0326) + .put("0288", "三菱UFJ信託銀行") + .put("0289", "みずほ信託銀行") + .put("0294", "三井住友信託銀行") + .put("0295", "BNYM信託") + .put("0297", "日本マスタートラスト信託銀行") + .put("0299", "ステート信託") + .put("0300", "SMBC信託銀行 プレスティア") + .put("0304", "野村信託銀行") + .put("0307", "オリックス銀行") + .put("0310", "GMOあおぞらネット銀行") + .put("0311", "農中信託") + .put("0320", "新生信託") + .put("0321", "日証金信託") + .put("0324", "日本トラスティサービス信託銀行") + .put("0325", "資産管理サービス信託銀行") + // }}} + // {{{ 旧長期信用銀行 (0397 ~ 0398) + .put("0397", "新生銀行") + .put("0398", "あおぞら銀行") + // }}} + // {{{ foreign banks (0400 ~ 0497) + .put("0401", "シティバンク、エヌ・エイ 銀行") + .put("0402", "JPモルガン・チェース銀行") + .put("0403", "アメリカ銀行") + .put("0411", "香港上海銀行") + .put("0413", "スタンチヤート") + .put("0414", "バークレイズ") + .put("0421", "アグリコル") + .put("0423", "ハナ") + .put("0424", "印度") + .put("0425", "兆豐國際商銀") + .put("0426", "バンコツク") + .put("0429", "バンクネガラ") + .put("0430", "ドイツ銀行") + .put("0432", "ブラジル") + .put("0438", "ユーオバシーズ") + .put("0439", "ユービーエス") + .put("0442", "BNYメロン") + .put("0443", "ビー・エヌ・ピー・パリバ銀行") + .put("0444", "チヤイニーズ") + .put("0445", "ソシエテ") + .put("0456", "ユバフ") + .put("0458", "DBS") + .put("0459", "パキスタン") + .put("0460", "クレデイスイス") + .put("0461", "コメルツ銀行") + .put("0463", "ウニクレデイト") + .put("0468", "インドステイト") + .put("0471", "カナダロイヤル") + .put("0472", "SBJ銀行") + .put("0477", "ウリイ") + .put("0482", "アイエヌジー") + .put("0484", "ナツトオース") + .put("0485", "アンズバンク") + .put("0487", "コモンウエルス") + .put("0489", "バンクチヤイナ") + .put("0495", "ステストリート") + .put("0498", "中小企業") + // }}} + // {{{ 第二地方銀行 (0501 ~ 0597) + .put("0501", "北洋銀行") + .put("0508", "きらやか銀行") + .put("0509", "北日本銀行") + .put("0512", "仙台銀行") + .put("0513", "福島銀行") + .put("0514", "大東銀行") + .put("0516", "東和銀行") + .put("0517", "栃木銀行") + .put("0522", "京葉銀行") + .put("0525", "東日本銀行") + .put("0526", "東京スター銀行") + .put("0530", "神奈川銀行") + .put("0532", "大光銀行") + .put("0533", "長野銀行") + .put("0534", "富山第一銀行") + .put("0537", "福邦銀行") + .put("0538", "静岡中央銀行") + .put("0542", "愛知銀行") + .put("0543", "名古屋銀行") + .put("0544", "中京銀行") + .put("0546", "第三銀行") + .put("0555", "大正銀行") + .put("0562", "みなと銀行") + .put("0565", "島根銀行") + .put("0566", "トマト銀行") + .put("0569", "もみじ銀行") + .put("0570", "西京銀行") + .put("0572", "徳島銀行") + .put("0573", "香川銀行") + .put("0576", "愛媛銀行") + .put("0578", "高知銀行") + .put("0582", "福岡中央銀行") + .put("0583", "佐賀共栄銀行") + .put("0585", "長崎銀行") + .put("0587", "熊本銀行") + .put("0590", "豊和銀行") + .put("0591", "宮崎太陽銀行") + .put("0594", "南日本銀行") + .put("0596", "沖縄海邦銀行") + // }}} + // {{{ more foreign banks (0600 ~ 0999) + .put("0603", "韓国産業") + .put("0607", "彰化商業") + .put("0608", "ウエルズフアゴ") + .put("0611", "第一商業") + .put("0612", "台湾") + .put("0615", "交通") + .put("0616", "メトロポリタン") + .put("0617", "フイリピン") + .put("0619", "中国工商") + .put("0621", "中國信託商業") + .put("0623", "インテーザ") + .put("0624", "國民") + .put("0625", "中国建設") + .put("0626", "イタウウニ") + .put("0627", "BBVA") + .put("0630", "中国農業") + .put("0631", "台新") + .put("0632", "玉山") + .put("0633", "台湾企銀") + .put("0808", "ドイツ証券") + .put("0813", "ソシエテ証券") + .put("0821", "ビーピー証券") + .put("0822", "バークレイ証券") + .put("0831", "アグリコル証券") + .put("0832", "ジエイピー証券") + .put("0842", "ゴルドマン証券") + .put("0845", "ナツトウエ証券") + .put("0900", "日本相互証券") + .put("0905", "東京金融取引所") + .put("0909", "日本クリア機構") + .put("0910", "ほふりクリア") + .put("0964", "しんきん証券") + .put("0966", "HSBC証券") + .put("0968", "セント東短証券") + .put("0971", "UBS証券") + .put("0972", "メリル日本証券") + // }}} + .build(); // minor ~280 lesser known banks - private static final Map minorBanksJapanese = ImmutableMap. builder() - // {{{ 信用金庫 (1001 ~ 1996) - .put("1000", "信金中央金庫") - .put("1001", "北海道信金") - .put("1003", "室蘭信金") - .put("1004", "空知信金") - .put("1006", "苫小牧信金") - .put("1008", "北門信金") - .put("1009", "伊達信金") - .put("1010", "北空知信金") - .put("1011", "日高信金") - .put("1013", "渡島信金") - .put("1014", "道南うみ街信金") - .put("1020", "旭川信金") - .put("1021", "稚内信金") - .put("1022", "留萌信金") - .put("1024", "北星信金") - .put("1026", "帯広信金") - .put("1027", "釧路信金") - .put("1028", "大地みらい信金") - .put("1030", "北見信金") - .put("1031", "網走信金") - .put("1033", "遠軽信金") - .put("1104", "東奥信金") - .put("1105", "青い森信金") - .put("1120", "秋田信金") - .put("1123", "羽後信金") - .put("1140", "山形信金") - .put("1141", "米沢信金") - .put("1142", "鶴岡信金") - .put("1143", "新庄信金") - .put("1150", "盛岡信金") - .put("1152", "宮古信金") - .put("1153", "一関信金") - .put("1154", "北上信金") - .put("1155", "花巻信金") - .put("1156", "水沢信金") - .put("1170", "杜の都信金") - .put("1171", "宮城第一信金") - .put("1172", "石巻信金") - .put("1174", "仙南信金") - .put("1181", "会津信金") - .put("1182", "郡山信金") - .put("1184", "白河信金") - .put("1185", "須賀川信金") - .put("1186", "ひまわり信金") - .put("1188", "あぶくま信金") - .put("1189", "二本松信金") - .put("1190", "福島信金") - .put("1203", "高崎信金") - .put("1204", "桐生信金") - .put("1206", "アイオー信金") - .put("1208", "利根郡信金") - .put("1209", "館林信金") - .put("1210", "北群馬信金") - .put("1211", "しののめ信金") - .put("1221", "足利小山信金") - .put("1222", "栃木信金") - .put("1223", "鹿沼相互信金") - .put("1224", "佐野信金") - .put("1225", "大田原信金") - .put("1227", "烏山信金") - .put("1240", "水戸信金") - .put("1242", "結城信金") - .put("1250", "埼玉県信金") - .put("1251", "川口信金") - .put("1252", "青木信金") - .put("1253", "飯能信金") - .put("1260", "千葉信金") - .put("1261", "銚子信金") - .put("1262", "東京ベイ信金") - .put("1264", "館山信金") - .put("1267", "佐原信金") - .put("1280", "横浜信金") - .put("1281", "かながわ信金") - .put("1282", "湘南信金") - .put("1283", "川崎信金") - .put("1286", "平塚信金") - .put("1288", "さがみ信金") - .put("1289", "中栄信金") - .put("1290", "中南信金") - .put("1303", "朝日信金") - .put("1305", "興産信金") - .put("1310", "さわやか信金") - .put("1311", "東京シテイ信金") - .put("1319", "芝信金") - .put("1320", "東京東信金") - .put("1321", "東栄信金") - .put("1323", "亀有信金") - .put("1326", "小松川信金") - .put("1327", "足立成和信金") - .put("1333", "東京三協信金") - .put("1336", "西京信金") - .put("1341", "西武信金") - .put("1344", "城南信金") - .put("1345", "東京)昭和信金") - .put("1346", "目黒信金") - .put("1348", "世田谷信金") - .put("1349", "東京信金") - .put("1351", "城北信金") - .put("1352", "滝野川信金") - .put("1356", "巣鴨信金") - .put("1358", "青梅信金") - .put("1360", "多摩信金") - .put("1370", "新潟信金") - .put("1371", "長岡信金") - .put("1373", "三条信金") - .put("1374", "新発田信金") - .put("1375", "柏崎信金") - .put("1376", "上越信金") - .put("1377", "新井信金") - .put("1379", "村上信金") - .put("1380", "加茂信金") - .put("1385", "甲府信金") - .put("1386", "山梨信金") - .put("1390", "長野信金") - .put("1391", "松本信金") - .put("1392", "上田信金") - .put("1393", "諏訪信金") - .put("1394", "飯田信金") - .put("1396", "アルプス信金") - .put("1401", "富山信金") - .put("1402", "高岡信金") - .put("1405", "にいかわ信金") - .put("1406", "氷見伏木信金") - .put("1412", "砺波信金") - .put("1413", "石動信金") - .put("1440", "金沢信金") - .put("1442", "のと共栄信金") - .put("1444", "北陸信金") - .put("1445", "鶴来信金") - .put("1448", "興能信金") - .put("1470", "福井信金") - .put("1471", "敦賀信金") - .put("1473", "小浜信金") - .put("1475", "越前信金") - .put("1501", "しず焼津信金") - .put("1502", "静清信金") - .put("1503", "浜松磐田信金") - .put("1505", "沼津信金") - .put("1506", "三島信金") - .put("1507", "富士宮信金") - .put("1513", "島田掛川信金") - .put("1515", "静岡)富士信金") - .put("1517", "遠州信金") - .put("1530", "岐阜信金") - .put("1531", "大垣西濃信金") - .put("1532", "高山信金") - .put("1533", "東濃信金") - .put("1534", "関信金") - .put("1538", "八幡信金") - .put("1550", "愛知信金") - .put("1551", "豊橋信金") - .put("1552", "岡崎信金") - .put("1553", "いちい信金") - .put("1554", "瀬戸信金") - .put("1555", "半田信金") - .put("1556", "知多信金") - .put("1557", "豊川信金") - .put("1559", "豊田信金") - .put("1560", "碧海信金") - .put("1561", "西尾信金") - .put("1562", "蒲郡信金") - .put("1563", "尾西信金") - .put("1565", "中日信金") - .put("1566", "東春信金") - .put("1580", "津信金") - .put("1581", "北伊勢上野信金") - .put("1583", "桑名三重信金") - .put("1585", "紀北信金") - .put("1602", "滋賀中央信金") - .put("1603", "長浜信金") - .put("1604", "湖東信金") - .put("1610", "京都信金") - .put("1611", "京都中央信金") - .put("1620", "京都北都信金") - .put("1630", "大阪信金") - .put("1633", "大阪厚生信金") - .put("1635", "大阪シテイ信金") - .put("1636", "大阪商工信金") - .put("1643", "永和信金") - .put("1645", "北おおさか信金") - .put("1656", "枚方信金") - .put("1666", "奈良信金") - .put("1667", "大和信金") - .put("1668", "奈良中央信金") - .put("1671", "新宮信金") - .put("1674", "きのくに信金") - .put("1680", "神戸信金") - .put("1685", "姫路信金") - .put("1686", "播州信金") - .put("1687", "兵庫信金") - .put("1688", "尼崎信金") - .put("1689", "日新信金") - .put("1691", "淡路信金") - .put("1692", "但馬信金") - .put("1694", "西兵庫信金") - .put("1695", "中兵庫信金") - .put("1696", "但陽信金") - .put("1701", "鳥取信金") - .put("1702", "米子信金") - .put("1703", "倉吉信金") - .put("1710", "しまね信金") - .put("1711", "日本海信金") - .put("1712", "島根中央信金") - .put("1732", "おかやま信金") - .put("1734", "水島信金") - .put("1735", "津山信金") - .put("1738", "玉島信金") - .put("1740", "備北信金") - .put("1741", "吉備信金") - .put("1742", "日生信金") - .put("1743", "備前信金") - .put("1750", "広島信金") - .put("1752", "呉信金") - .put("1756", "しまなみ信金") - .put("1758", "広島みどり信金") - .put("1780", "萩山口信金") - .put("1781", "西中国信金") - .put("1789", "東山口信金") - .put("1801", "徳島信金") - .put("1803", "阿南信金") - .put("1830", "高松信金") - .put("1833", "観音寺信金") - .put("1860", "愛媛信金") - .put("1862", "宇和島信金") - .put("1864", "東予信金") - .put("1866", "川之江信金") - .put("1880", "幡多信金") - .put("1881", "高知信金") - .put("1901", "福岡信金") - .put("1903", "福岡ひびき信金") - .put("1908", "大牟田柳川信金") - .put("1909", "筑後信金") - .put("1910", "飯塚信金") - .put("1917", "大川信金") - .put("1920", "遠賀信金") - .put("1930", "唐津信金") - .put("1931", "佐賀信金") - .put("1933", "九州ひぜん信金") - .put("1942", "たちばな信金") - .put("1951", "熊本信金") - .put("1952", "熊本第一信金") - .put("1954", "熊本中央信金") - .put("1960", "大分信金") - .put("1962", "大分みらい信金") - .put("1980", "宮崎都城信金") - .put("1985", "高鍋信金") - .put("1990", "鹿児島信金") - .put("1991", "鹿児島相互信金") - .put("1993", "奄美大島信金") - .put("1996", "コザ信金") - // }}} - // {{{ 信用組合 (2011 ~ 2895) - .put("2004", "商工組合中央金庫") - .put("2010", "全国信用協同組合連合会") - .put("2213", "整理回収機構") - // }}} - // {{{ 労働金庫 (2951 ~ 2997) - .put("2950", "労働金庫連合会") - // }}} - // {{{ 農林中央金庫 (3000) - .put("3000", "農林中央金庫") - // }}} - // {{{ 信用農業協同組合連合会 (3001 ~ 3046) - .put("3001", "北海道信用農業協同組合連合会") - .put("3003", "岩手県信用農業協同組合連合会") - .put("3008", "茨城県信用農業協同組合連合会") - .put("3011", "埼玉県信用農業協同組合連合会") - .put("3013", "東京都信用農業協同組合連合会") - .put("3014", "神奈川県信用農業協同組合連合会") - .put("3015", "山梨県信用農業協同組合連合会") - .put("3016", "長野県信用農業協同組合連合会") - .put("3017", "新潟県信用農業協同組合連合会") - .put("3019", "石川県信用農業協同組合連合会") - .put("3020", "岐阜県信用農業協同組合連合会") - .put("3021", "静岡県信用農業協同組合連合会") - .put("3022", "愛知県信用農業協同組合連合会") - .put("3023", "三重県信用農業協同組合連合会") - .put("3024", "福井県信用農業協同組合連合会") - .put("3025", "滋賀県信用農業協同組合連合会") - .put("3026", "京都府信用農業協同組合連合会") - .put("3027", "大阪府信用農業協同組合連合会") - .put("3028", "兵庫県信用農業協同組合連合会") - .put("3030", "和歌山県信用農業協同組合連合会") - .put("3031", "鳥取県信用農業協同組合連合会") - .put("3034", "広島県信用農業協同組合連合会") - .put("3035", "山口県信用農業協同組合連合会") - .put("3036", "徳島県信用農業協同組合連合会") - .put("3037", "香川県信用農業協同組合連合会") - .put("3038", "愛媛県信用農業協同組合連合会") - .put("3039", "高知県信用農業協同組合連合会") - .put("3040", "福岡県信用農業協同組合連合会") - .put("3041", "佐賀県信用農業協同組合連合会") - .put("3044", "大分県信用農業協同組合連合会") - .put("3045", "宮崎県信用農業協同組合連合会") - .put("3046", "鹿児島県信用農業協同組合連合会") - // }}} - // {{{ "JA Bank" agricultural cooperative associations (3056 ~ 9375) - // REMOVED: the farmers should use a real bank if they want to sell bitcoin - // }}} - // {{{ 信用漁業協同組合連合会 (9450 ~ 9496) - .put("9450", "北海道信用漁業協同組合連合会") - .put("9451", "青森県信用漁業協同組合連合会") - .put("9452", "岩手県信用漁業協同組合連合会") - .put("9453", "宮城県漁業協同組合") - .put("9456", "福島県信用漁業協同組合連合会") - .put("9457", "茨城県信用漁業協同組合連合会") - .put("9461", "千葉県信用漁業協同組合連合会") - .put("9462", "東京都信用漁業協同組合連合会") - .put("9466", "新潟県信用漁業協同組合連合会") - .put("9467", "富山県信用漁業協同組合連合会") - .put("9468", "石川県信用漁業協同組合連合会") - .put("9470", "静岡県信用漁業協同組合連合会") - .put("9471", "愛知県信用漁業協同組合連合会") - .put("9472", "三重県信用漁業協同組合連合会") - .put("9473", "福井県信用漁業協同組合連合会") - .put("9475", "京都府信用漁業協同組合連合会") - .put("9477", "なぎさ信用漁業協同組合連合会") - .put("9480", "鳥取県信用漁業協同組合連合会") - .put("9481", "JFしまね漁業協同組合") - .put("9483", "広島県信用漁業協同組合連合会") - .put("9484", "山口県漁業協同組合") - .put("9485", "徳島県信用漁業協同組合連合会") - .put("9486", "香川県信用漁業協同組合連合会") - .put("9487", "愛媛県信用漁業協同組合連合会") - .put("9488", "高知県信用漁業協同組合連合会") - .put("9489", "福岡県信用漁業協同組合連合会") - .put("9490", "佐賀県信用漁業協同組合連合会") - .put("9491", "長崎県信用漁業協同組合連合会") - .put("9493", "大分県漁業協同組合") - .put("9494", "宮崎県信用漁業協同組合連合会") - .put("9495", "鹿児島県信用漁業協同組合連合会") - .put("9496", "沖縄県信用漁業協同組合連合会") - // }}} - // {{{ securities firms - .put("9500", "東京短資") - .put("9501", "セントラル短資") - .put("9507", "上田八木短資") - .put("9510", "日本証券金融") - .put("9520", "野村証券") - .put("9521", "日興証券") - .put("9523", "大和証券") - .put("9524", "みずほ証券") - .put("9528", "岡三証券") - .put("9530", "岩井コスモ証券") - .put("9532", "三菱UFJ証券") - .put("9534", "丸三証券") - .put("9535", "東洋証券") - .put("9537", "水戸証券") - .put("9539", "東海東京証券") - .put("9542", "むさし証券") - .put("9545", "いちよし証券") - .put("9573", "極東証券") - .put("9574", "立花証券") - .put("9579", "光世証券") - .put("9584", "ちばぎん証券") - .put("9589", "シテイ証券") - .put("9594", "CS証券") - .put("9595", "スタンレー証券") - .put("9930", "日本政策投資") - .put("9932", "政策金融公庫") - .put("9933", "国際協力") - .put("9945", "預金保険機構") - // }}} - .build(); + private static final Map minorBanksJapanese = ImmutableMap.builder() + // {{{ 信用金庫 (1001 ~ 1996) + .put("1000", "信金中央金庫") + .put("1001", "北海道信金") + .put("1003", "室蘭信金") + .put("1004", "空知信金") + .put("1006", "苫小牧信金") + .put("1008", "北門信金") + .put("1009", "伊達信金") + .put("1010", "北空知信金") + .put("1011", "日高信金") + .put("1013", "渡島信金") + .put("1014", "道南うみ街信金") + .put("1020", "旭川信金") + .put("1021", "稚内信金") + .put("1022", "留萌信金") + .put("1024", "北星信金") + .put("1026", "帯広信金") + .put("1027", "釧路信金") + .put("1028", "大地みらい信金") + .put("1030", "北見信金") + .put("1031", "網走信金") + .put("1033", "遠軽信金") + .put("1104", "東奥信金") + .put("1105", "青い森信金") + .put("1120", "秋田信金") + .put("1123", "羽後信金") + .put("1140", "山形信金") + .put("1141", "米沢信金") + .put("1142", "鶴岡信金") + .put("1143", "新庄信金") + .put("1150", "盛岡信金") + .put("1152", "宮古信金") + .put("1153", "一関信金") + .put("1154", "北上信金") + .put("1155", "花巻信金") + .put("1156", "水沢信金") + .put("1170", "杜の都信金") + .put("1171", "宮城第一信金") + .put("1172", "石巻信金") + .put("1174", "仙南信金") + .put("1181", "会津信金") + .put("1182", "郡山信金") + .put("1184", "白河信金") + .put("1185", "須賀川信金") + .put("1186", "ひまわり信金") + .put("1188", "あぶくま信金") + .put("1189", "二本松信金") + .put("1190", "福島信金") + .put("1203", "高崎信金") + .put("1204", "桐生信金") + .put("1206", "アイオー信金") + .put("1208", "利根郡信金") + .put("1209", "館林信金") + .put("1210", "北群馬信金") + .put("1211", "しののめ信金") + .put("1221", "足利小山信金") + .put("1222", "栃木信金") + .put("1223", "鹿沼相互信金") + .put("1224", "佐野信金") + .put("1225", "大田原信金") + .put("1227", "烏山信金") + .put("1240", "水戸信金") + .put("1242", "結城信金") + .put("1250", "埼玉県信金") + .put("1251", "川口信金") + .put("1252", "青木信金") + .put("1253", "飯能信金") + .put("1260", "千葉信金") + .put("1261", "銚子信金") + .put("1262", "東京ベイ信金") + .put("1264", "館山信金") + .put("1267", "佐原信金") + .put("1280", "横浜信金") + .put("1281", "かながわ信金") + .put("1282", "湘南信金") + .put("1283", "川崎信金") + .put("1286", "平塚信金") + .put("1288", "さがみ信金") + .put("1289", "中栄信金") + .put("1290", "中南信金") + .put("1303", "朝日信金") + .put("1305", "興産信金") + .put("1310", "さわやか信金") + .put("1311", "東京シテイ信金") + .put("1319", "芝信金") + .put("1320", "東京東信金") + .put("1321", "東栄信金") + .put("1323", "亀有信金") + .put("1326", "小松川信金") + .put("1327", "足立成和信金") + .put("1333", "東京三協信金") + .put("1336", "西京信金") + .put("1341", "西武信金") + .put("1344", "城南信金") + .put("1345", "東京)昭和信金") + .put("1346", "目黒信金") + .put("1348", "世田谷信金") + .put("1349", "東京信金") + .put("1351", "城北信金") + .put("1352", "滝野川信金") + .put("1356", "巣鴨信金") + .put("1358", "青梅信金") + .put("1360", "多摩信金") + .put("1370", "新潟信金") + .put("1371", "長岡信金") + .put("1373", "三条信金") + .put("1374", "新発田信金") + .put("1375", "柏崎信金") + .put("1376", "上越信金") + .put("1377", "新井信金") + .put("1379", "村上信金") + .put("1380", "加茂信金") + .put("1385", "甲府信金") + .put("1386", "山梨信金") + .put("1390", "長野信金") + .put("1391", "松本信金") + .put("1392", "上田信金") + .put("1393", "諏訪信金") + .put("1394", "飯田信金") + .put("1396", "アルプス信金") + .put("1401", "富山信金") + .put("1402", "高岡信金") + .put("1405", "にいかわ信金") + .put("1406", "氷見伏木信金") + .put("1412", "砺波信金") + .put("1413", "石動信金") + .put("1440", "金沢信金") + .put("1442", "のと共栄信金") + .put("1444", "北陸信金") + .put("1445", "鶴来信金") + .put("1448", "興能信金") + .put("1470", "福井信金") + .put("1471", "敦賀信金") + .put("1473", "小浜信金") + .put("1475", "越前信金") + .put("1501", "しず焼津信金") + .put("1502", "静清信金") + .put("1503", "浜松磐田信金") + .put("1505", "沼津信金") + .put("1506", "三島信金") + .put("1507", "富士宮信金") + .put("1513", "島田掛川信金") + .put("1515", "静岡)富士信金") + .put("1517", "遠州信金") + .put("1530", "岐阜信金") + .put("1531", "大垣西濃信金") + .put("1532", "高山信金") + .put("1533", "東濃信金") + .put("1534", "関信金") + .put("1538", "八幡信金") + .put("1550", "愛知信金") + .put("1551", "豊橋信金") + .put("1552", "岡崎信金") + .put("1553", "いちい信金") + .put("1554", "瀬戸信金") + .put("1555", "半田信金") + .put("1556", "知多信金") + .put("1557", "豊川信金") + .put("1559", "豊田信金") + .put("1560", "碧海信金") + .put("1561", "西尾信金") + .put("1562", "蒲郡信金") + .put("1563", "尾西信金") + .put("1565", "中日信金") + .put("1566", "東春信金") + .put("1580", "津信金") + .put("1581", "北伊勢上野信金") + .put("1583", "桑名三重信金") + .put("1585", "紀北信金") + .put("1602", "滋賀中央信金") + .put("1603", "長浜信金") + .put("1604", "湖東信金") + .put("1610", "京都信金") + .put("1611", "京都中央信金") + .put("1620", "京都北都信金") + .put("1630", "大阪信金") + .put("1633", "大阪厚生信金") + .put("1635", "大阪シテイ信金") + .put("1636", "大阪商工信金") + .put("1643", "永和信金") + .put("1645", "北おおさか信金") + .put("1656", "枚方信金") + .put("1666", "奈良信金") + .put("1667", "大和信金") + .put("1668", "奈良中央信金") + .put("1671", "新宮信金") + .put("1674", "きのくに信金") + .put("1680", "神戸信金") + .put("1685", "姫路信金") + .put("1686", "播州信金") + .put("1687", "兵庫信金") + .put("1688", "尼崎信金") + .put("1689", "日新信金") + .put("1691", "淡路信金") + .put("1692", "但馬信金") + .put("1694", "西兵庫信金") + .put("1695", "中兵庫信金") + .put("1696", "但陽信金") + .put("1701", "鳥取信金") + .put("1702", "米子信金") + .put("1703", "倉吉信金") + .put("1710", "しまね信金") + .put("1711", "日本海信金") + .put("1712", "島根中央信金") + .put("1732", "おかやま信金") + .put("1734", "水島信金") + .put("1735", "津山信金") + .put("1738", "玉島信金") + .put("1740", "備北信金") + .put("1741", "吉備信金") + .put("1742", "日生信金") + .put("1743", "備前信金") + .put("1750", "広島信金") + .put("1752", "呉信金") + .put("1756", "しまなみ信金") + .put("1758", "広島みどり信金") + .put("1780", "萩山口信金") + .put("1781", "西中国信金") + .put("1789", "東山口信金") + .put("1801", "徳島信金") + .put("1803", "阿南信金") + .put("1830", "高松信金") + .put("1833", "観音寺信金") + .put("1860", "愛媛信金") + .put("1862", "宇和島信金") + .put("1864", "東予信金") + .put("1866", "川之江信金") + .put("1880", "幡多信金") + .put("1881", "高知信金") + .put("1901", "福岡信金") + .put("1903", "福岡ひびき信金") + .put("1908", "大牟田柳川信金") + .put("1909", "筑後信金") + .put("1910", "飯塚信金") + .put("1917", "大川信金") + .put("1920", "遠賀信金") + .put("1930", "唐津信金") + .put("1931", "佐賀信金") + .put("1933", "九州ひぜん信金") + .put("1942", "たちばな信金") + .put("1951", "熊本信金") + .put("1952", "熊本第一信金") + .put("1954", "熊本中央信金") + .put("1960", "大分信金") + .put("1962", "大分みらい信金") + .put("1980", "宮崎都城信金") + .put("1985", "高鍋信金") + .put("1990", "鹿児島信金") + .put("1991", "鹿児島相互信金") + .put("1993", "奄美大島信金") + .put("1996", "コザ信金") + // }}} + // {{{ 信用組合 (2011 ~ 2895) + .put("2004", "商工組合中央金庫") + .put("2010", "全国信用協同組合連合会") + .put("2213", "整理回収機構") + // }}} + // {{{ 労働金庫 (2951 ~ 2997) + .put("2950", "労働金庫連合会") + // }}} + // {{{ 農林中央金庫 (3000) + .put("3000", "農林中央金庫") + // }}} + // {{{ 信用農業協同組合連合会 (3001 ~ 3046) + .put("3001", "北海道信用農業協同組合連合会") + .put("3003", "岩手県信用農業協同組合連合会") + .put("3008", "茨城県信用農業協同組合連合会") + .put("3011", "埼玉県信用農業協同組合連合会") + .put("3013", "東京都信用農業協同組合連合会") + .put("3014", "神奈川県信用農業協同組合連合会") + .put("3015", "山梨県信用農業協同組合連合会") + .put("3016", "長野県信用農業協同組合連合会") + .put("3017", "新潟県信用農業協同組合連合会") + .put("3019", "石川県信用農業協同組合連合会") + .put("3020", "岐阜県信用農業協同組合連合会") + .put("3021", "静岡県信用農業協同組合連合会") + .put("3022", "愛知県信用農業協同組合連合会") + .put("3023", "三重県信用農業協同組合連合会") + .put("3024", "福井県信用農業協同組合連合会") + .put("3025", "滋賀県信用農業協同組合連合会") + .put("3026", "京都府信用農業協同組合連合会") + .put("3027", "大阪府信用農業協同組合連合会") + .put("3028", "兵庫県信用農業協同組合連合会") + .put("3030", "和歌山県信用農業協同組合連合会") + .put("3031", "鳥取県信用農業協同組合連合会") + .put("3034", "広島県信用農業協同組合連合会") + .put("3035", "山口県信用農業協同組合連合会") + .put("3036", "徳島県信用農業協同組合連合会") + .put("3037", "香川県信用農業協同組合連合会") + .put("3038", "愛媛県信用農業協同組合連合会") + .put("3039", "高知県信用農業協同組合連合会") + .put("3040", "福岡県信用農業協同組合連合会") + .put("3041", "佐賀県信用農業協同組合連合会") + .put("3044", "大分県信用農業協同組合連合会") + .put("3045", "宮崎県信用農業協同組合連合会") + .put("3046", "鹿児島県信用農業協同組合連合会") + // }}} + // {{{ "JA Bank" agricultural cooperative associations (3056 ~ 9375) + // REMOVED: the farmers should use a real bank if they want to sell bitcoin + // }}} + // {{{ 信用漁業協同組合連合会 (9450 ~ 9496) + .put("9450", "北海道信用漁業協同組合連合会") + .put("9451", "青森県信用漁業協同組合連合会") + .put("9452", "岩手県信用漁業協同組合連合会") + .put("9453", "宮城県漁業協同組合") + .put("9456", "福島県信用漁業協同組合連合会") + .put("9457", "茨城県信用漁業協同組合連合会") + .put("9461", "千葉県信用漁業協同組合連合会") + .put("9462", "東京都信用漁業協同組合連合会") + .put("9466", "新潟県信用漁業協同組合連合会") + .put("9467", "富山県信用漁業協同組合連合会") + .put("9468", "石川県信用漁業協同組合連合会") + .put("9470", "静岡県信用漁業協同組合連合会") + .put("9471", "愛知県信用漁業協同組合連合会") + .put("9472", "三重県信用漁業協同組合連合会") + .put("9473", "福井県信用漁業協同組合連合会") + .put("9475", "京都府信用漁業協同組合連合会") + .put("9477", "なぎさ信用漁業協同組合連合会") + .put("9480", "鳥取県信用漁業協同組合連合会") + .put("9481", "JFしまね漁業協同組合") + .put("9483", "広島県信用漁業協同組合連合会") + .put("9484", "山口県漁業協同組合") + .put("9485", "徳島県信用漁業協同組合連合会") + .put("9486", "香川県信用漁業協同組合連合会") + .put("9487", "愛媛県信用漁業協同組合連合会") + .put("9488", "高知県信用漁業協同組合連合会") + .put("9489", "福岡県信用漁業協同組合連合会") + .put("9490", "佐賀県信用漁業協同組合連合会") + .put("9491", "長崎県信用漁業協同組合連合会") + .put("9493", "大分県漁業協同組合") + .put("9494", "宮崎県信用漁業協同組合連合会") + .put("9495", "鹿児島県信用漁業協同組合連合会") + .put("9496", "沖縄県信用漁業協同組合連合会") + // }}} + // {{{ securities firms + .put("9500", "東京短資") + .put("9501", "セントラル短資") + .put("9507", "上田八木短資") + .put("9510", "日本証券金融") + .put("9520", "野村証券") + .put("9521", "日興証券") + .put("9523", "大和証券") + .put("9524", "みずほ証券") + .put("9528", "岡三証券") + .put("9530", "岩井コスモ証券") + .put("9532", "三菱UFJ証券") + .put("9534", "丸三証券") + .put("9535", "東洋証券") + .put("9537", "水戸証券") + .put("9539", "東海東京証券") + .put("9542", "むさし証券") + .put("9545", "いちよし証券") + .put("9573", "極東証券") + .put("9574", "立花証券") + .put("9579", "光世証券") + .put("9584", "ちばぎん証券") + .put("9589", "シテイ証券") + .put("9594", "CS証券") + .put("9595", "スタンレー証券") + .put("9930", "日本政策投資") + .put("9932", "政策金融公庫") + .put("9933", "国際協力") + .put("9945", "預金保険機構") + // }}} + .build(); private final static String ID_OPEN = ""; private final static String ID_CLOSE = ""; @@ -788,12 +784,10 @@ public class JapanBankData // don't localize these strings into all languages, // all we want is either Japanese or English here. - public static final String getString(String id) - { + public static String getString(String id) { boolean ja = GUIUtil.getUserLanguage().equals("ja"); - switch (id) - { + switch (id) { case "bank": if (ja) return "銀行名 ・金融機関名"; return "Bank or Financial Institution"; @@ -865,21 +859,20 @@ public class JapanBankData case "japanese.validation.regex": // epic regex to only match Japanese input return "[" + // match any of these characters: - // "A-z" + // full-width alphabet - // "0-9" + // full-width numerals - "一-龯" + // all Japanese kanji (0x4e00 ~ 0x9fcf) - "ぁ-ゔ" + // full-width hiragana (0x3041 ~ 0x3094) - "ァ-・" + // full-width katakana (0x30a1 ~ 0x30fb) - "ぁ-ゞ" + // half-width hiragana - "ァ-ン゙゚" + // half-width katakana - "ヽヾ゛゜ー" + // 0x309e, 0x309b, 0x309c, 0x30fc - " " + // full-width space - " " + // half-width space - "]+"; // for any length + // "A-z" + // full-width alphabet + // "0-9" + // full-width numerals + "一-龯" + // common Japanese kanji (0x4e00 ~ 0x9faf) + "々" + // kanji iteration mark (0x3005) + "〇" + // kanji number zero (0x3007) + "ぁ-ゞ" + // hiragana (0x3041 ~ 0x309e) + "ァ-・" + // full-width katakana (0x30a1 ~ 0x30fb) + "ァ-ン゙゚" + // half-width katakana + "ヽヾ゛゜ー" + // 0x30fd, 0x30fe, 0x309b, 0x309c, 0x30fc + " " + // full-width space + " " + // half-width space + "]+"; // for any length } return "null"; } } - -// vim:ts=4:sw=4:expandtab:foldmethod=marker:nowrap: diff --git a/desktop/src/main/java/bisq/desktop/images.css b/desktop/src/main/java/bisq/desktop/images.css index f943661fed..175bd56fc7 100644 --- a/desktop/src/main/java/bisq/desktop/images.css +++ b/desktop/src/main/java/bisq/desktop/images.css @@ -29,6 +29,10 @@ -fx-image: url("../../images/remove.png"); } +#image-edit { + -fx-image: url("../../images/edit.png"); +} + #image-buy-white { -fx-image: url("../../images/buy_white.png"); } diff --git a/desktop/src/main/java/bisq/desktop/main/MainView.java b/desktop/src/main/java/bisq/desktop/main/MainView.java index c893b69b62..431eaf1627 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainView.java +++ b/desktop/src/main/java/bisq/desktop/main/MainView.java @@ -107,7 +107,7 @@ import static javafx.scene.layout.AnchorPane.setTopAnchor; @FxmlView @Slf4j -public class MainView extends InitializableView { +public class MainView extends InitializableView { // If after 30 sec we have not got connected we show "open network settings" button private final static int SHOW_TOR_SETTINGS_DELAY_SEC = 90; @Setter @@ -170,8 +170,8 @@ public class MainView extends InitializableView { MainView.rootContainer.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT); ToggleButton marketButton = new NavButton(MarketView.class, Res.get("mainView.menu.market").toUpperCase()); - ToggleButton buyButton = new NavButton(BuyOfferView.class, Res.get("mainView.menu.buyBtc").toUpperCase()); - ToggleButton sellButton = new NavButton(SellOfferView.class, Res.get("mainView.menu.sellBtc").toUpperCase()); + ToggleButton buyButton = new NavButton(BuyOfferView.class, Res.get("mainView.menu.buy").toUpperCase()); + ToggleButton sellButton = new NavButton(SellOfferView.class, Res.get("mainView.menu.sell").toUpperCase()); ToggleButton portfolioButton = new NavButton(PortfolioView.class, Res.get("mainView.menu.portfolio").toUpperCase()); // ToggleButton fundsButton = new NavButton(FundsView.class, Res.get("mainView.menu.funds").toUpperCase()); @@ -182,7 +182,6 @@ public class MainView extends InitializableView { JFXBadge portfolioButtonWithBadge = new JFXBadge(portfolioButton); JFXBadge supportButtonWithBadge = new JFXBadge(supportButton); JFXBadge settingsButtonWithBadge = new JFXBadge(settingsButton); - settingsButtonWithBadge.getStyleClass().add("new"); Locale locale = GlobalSettings.getLocale(); DecimalFormat currencyFormat = (DecimalFormat) NumberFormat.getNumberInstance(locale); @@ -353,8 +352,9 @@ public class MainView extends InitializableView { baseApplicationContainer.setBottom(createFooter()); setupBadge(portfolioButtonWithBadge, model.getNumPendingTrades(), model.getShowPendingTradesNotification()); -// setupBadge(supportButtonWithBadge, model.getNumOpenSupportTickets(), model.getShowOpenSupportTicketsNotification()); + //setupBadge(supportButtonWithBadge, model.getNumOpenSupportTickets(), model.getShowOpenSupportTicketsNotification()); setupBadge(settingsButtonWithBadge, new SimpleStringProperty(Res.get("shared.new")), model.getShowSettingsUpdatesNotification()); + settingsButtonWithBadge.getStyleClass().add("new"); navigation.addListener((viewPath, data) -> { if (viewPath.size() != 2 || viewPath.indexOf(MainView.class) != 0) diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index bf23b17fd6..3b4b742567 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -228,7 +228,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener tradeManager.getObservableList().forEach(trade -> { Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); String key; - switch (trade.getTradePeriodState()) { + switch (trade.getPeriodState()) { case FIRST_HALF: break; case SECOND_HALF: diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java index 34184860d3..f1cf254c77 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java @@ -70,7 +70,7 @@ public abstract class PaymentAccountsView { if (newValue != null) - onSelectAccount(newValue); + onSelectAccount(oldValue, newValue); }; Label placeholder = new AutoTooltipLabel(Res.get("shared.noAccountsSetupYet")); placeholder.setWrapText(true); @@ -175,7 +175,8 @@ public abstract class PaymentAccountsView paymentAccount.getPaymentMethod().isAsset()) + .filter(paymentAccount -> paymentAccount.getPaymentMethod().isBlockchain()) .collect(Collectors.toList())); paymentAccounts.sort(Comparator.comparing(PaymentAccount::getAccountName)); } @@ -130,6 +130,11 @@ class AltCoinAccountsDataModel extends ActivatableDataModel { accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload()); } + public void onUpdateAccount(PaymentAccount paymentAccount) { + paymentAccount.onPersistChanges(); + user.requestPersistence(); + } + public boolean onDeleteAccount(PaymentAccount paymentAccount) { boolean isPaymentAccountUsed = openOfferManager.getObservableList().stream() .filter(o -> o.getOffer().getMakerPaymentAccountId().equals(paymentAccount.getId())) diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java index 8fadd469e0..04dbcf88d0 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java @@ -164,6 +164,16 @@ public class AltCoinAccountsView extends PaymentAccountsView tuple = add2ButtonsAfterGroup(root, ++gridRow, Res.get("shared.deleteAccount"), Res.get("shared.cancel")); - Button deleteAccountButton = tuple.first; - deleteAccountButton.setOnAction(event -> onDeleteAccount(paymentAccount)); - Button cancelButton = tuple.second; - cancelButton.setOnAction(event -> removeSelectAccountForm()); + Tuple3 tuple = add3ButtonsAfterGroup( + root, + ++gridRow, + Res.get("shared.save"), + Res.get("shared.deleteAccount"), + Res.get("shared.cancel") + ); + + Button saveAccountButton = tuple.first; + saveAccountButton.setOnAction(event -> onUpdateAccount(current)); + Button deleteAccountButton = tuple.second; + deleteAccountButton.setOnAction(event -> onDeleteAccount(current)); + Button cancelButton = tuple.third; + cancelButton.setOnAction(event -> onCancelSelectedAccount(current)); GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan()); - model.onSelectAccount(paymentAccount); + model.onSelectAccount(current); } diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsViewModel.java b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsViewModel.java index 55d4322244..6de2213c77 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsViewModel.java @@ -50,6 +50,10 @@ class AltCoinAccountsViewModel extends ActivatableWithDataModel list = user.getPaymentAccounts().stream() - .filter(paymentAccount -> !paymentAccount.getPaymentMethod().isAsset()) + .filter(paymentAccount -> !paymentAccount.getPaymentMethod().isBlockchain()) .collect(Collectors.toList()); paymentAccounts.setAll(list); paymentAccounts.sort(Comparator.comparing(PaymentAccount::getAccountName)); @@ -137,6 +137,11 @@ class FiatAccountsDataModel extends ActivatableDataModel { accountAgeWitnessService.signAndPublishSameNameAccounts(); } + public void onUpdateAccount(PaymentAccount paymentAccount) { + paymentAccount.onPersistChanges(); + user.requestPersistence(); + } + public boolean onDeleteAccount(PaymentAccount paymentAccount) { boolean isPaymentAccountUsed = openOfferManager.getObservableList().stream() .anyMatch(o -> o.getOffer().getMakerPaymentAccountId().equals(paymentAccount.getId())); diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java index b03030fef9..1a1b294547 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java @@ -19,35 +19,56 @@ package bisq.desktop.main.account.content.fiataccounts; import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.components.paymentmethods.AchTransferForm; import bisq.desktop.components.paymentmethods.AdvancedCashForm; import bisq.desktop.components.paymentmethods.AliPayForm; import bisq.desktop.components.paymentmethods.AmazonGiftCardForm; import bisq.desktop.components.paymentmethods.AustraliaPayidForm; +import bisq.desktop.components.paymentmethods.BizumForm; +import bisq.desktop.components.paymentmethods.CapitualForm; import bisq.desktop.components.paymentmethods.CashByMailForm; import bisq.desktop.components.paymentmethods.CashDepositForm; +import bisq.desktop.components.paymentmethods.CelPayForm; import bisq.desktop.components.paymentmethods.ChaseQuickPayForm; import bisq.desktop.components.paymentmethods.ClearXchangeForm; +import bisq.desktop.components.paymentmethods.DomesticWireTransferForm; import bisq.desktop.components.paymentmethods.F2FForm; import bisq.desktop.components.paymentmethods.FasterPaymentsForm; import bisq.desktop.components.paymentmethods.HalCashForm; +import bisq.desktop.components.paymentmethods.ImpsForm; import bisq.desktop.components.paymentmethods.InteracETransferForm; import bisq.desktop.components.paymentmethods.JapanBankTransferForm; +import bisq.desktop.components.paymentmethods.MoneseForm; import bisq.desktop.components.paymentmethods.MoneyBeamForm; import bisq.desktop.components.paymentmethods.MoneyGramForm; import bisq.desktop.components.paymentmethods.NationalBankForm; +import bisq.desktop.components.paymentmethods.NeftForm; +import bisq.desktop.components.paymentmethods.NequiForm; +import bisq.desktop.components.paymentmethods.PaxumForm; import bisq.desktop.components.paymentmethods.PaymentMethodForm; +import bisq.desktop.components.paymentmethods.PayseraForm; +import bisq.desktop.components.paymentmethods.PaytmForm; import bisq.desktop.components.paymentmethods.PerfectMoneyForm; +import bisq.desktop.components.paymentmethods.PixForm; import bisq.desktop.components.paymentmethods.PopmoneyForm; import bisq.desktop.components.paymentmethods.PromptPayForm; import bisq.desktop.components.paymentmethods.RevolutForm; +import bisq.desktop.components.paymentmethods.RtgsForm; import bisq.desktop.components.paymentmethods.SameBankForm; +import bisq.desktop.components.paymentmethods.SatispayForm; import bisq.desktop.components.paymentmethods.SepaForm; import bisq.desktop.components.paymentmethods.SepaInstantForm; import bisq.desktop.components.paymentmethods.SpecificBankForm; +import bisq.desktop.components.paymentmethods.StrikeForm; +import bisq.desktop.components.paymentmethods.SwiftForm; import bisq.desktop.components.paymentmethods.SwishForm; +import bisq.desktop.components.paymentmethods.TikkieForm; import bisq.desktop.components.paymentmethods.TransferwiseForm; +import bisq.desktop.components.paymentmethods.TransferwiseUsdForm; import bisq.desktop.components.paymentmethods.USPostalMoneyOrderForm; import bisq.desktop.components.paymentmethods.UpholdForm; +import bisq.desktop.components.paymentmethods.UpiForm; +import bisq.desktop.components.paymentmethods.VerseForm; import bisq.desktop.components.paymentmethods.WeChatPayForm; import bisq.desktop.components.paymentmethods.WesternUnionForm; import bisq.desktop.main.account.content.PaymentAccountsView; @@ -59,13 +80,14 @@ import bisq.desktop.util.validation.AdvancedCashValidator; import bisq.desktop.util.validation.AliPayValidator; import bisq.desktop.util.validation.AustraliaPayidValidator; import bisq.desktop.util.validation.BICValidator; +import bisq.desktop.util.validation.CapitualValidator; import bisq.desktop.util.validation.ChaseQuickPayValidator; import bisq.desktop.util.validation.ClearXchangeValidator; import bisq.desktop.util.validation.F2FValidator; import bisq.desktop.util.validation.HalCashValidator; -import bisq.desktop.util.validation.IBANValidator; import bisq.desktop.util.validation.InteracETransferValidator; import bisq.desktop.util.validation.JapanBankTransferValidator; +import bisq.desktop.util.validation.LengthValidator; import bisq.desktop.util.validation.MoneyBeamValidator; import bisq.desktop.util.validation.PerfectMoneyValidator; import bisq.desktop.util.validation.PopmoneyValidator; @@ -76,13 +98,12 @@ import bisq.desktop.util.validation.TransferwiseValidator; import bisq.desktop.util.validation.USPostalMoneyOrderValidator; import bisq.desktop.util.validation.UpholdValidator; import bisq.desktop.util.validation.WeChatPayValidator; -import bisq.desktop.util.validation.LengthValidator; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.Res; import bisq.core.offer.OfferRestrictions; import bisq.core.payment.AmazonGiftCardAccount; -import bisq.core.payment.AustraliaPayid; +import bisq.core.payment.AustraliaPayidAccount; import bisq.core.payment.CashByMailAccount; import bisq.core.payment.CashDepositAccount; import bisq.core.payment.ClearXchangeAccount; @@ -133,8 +154,8 @@ import static bisq.desktop.util.FormBuilder.addTopLabelListView; @FxmlView public class FiatAccountsView extends PaymentAccountsView { - private final IBANValidator ibanValidator; private final BICValidator bicValidator; + private final CapitualValidator capitualValidator; private final LengthValidator inputValidator; private final UpholdValidator upholdValidator; private final MoneyBeamValidator moneyBeamValidator; @@ -164,8 +185,8 @@ public class FiatAccountsView extends PaymentAccountsView GUIUtil.openWebPage("https://docs.bisq.network/trading-rules.html#f2f-trading")) + .onClose(() -> GUIUtil.openWebPage("https://bisq.wiki/Face-to-face_(payment_method)")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); @@ -325,7 +346,7 @@ public class FiatAccountsView extends PaymentAccountsView doSaveNewAccount(paymentAccount)) .show(); - } else if (paymentAccount instanceof AustraliaPayid) { + } else if (paymentAccount instanceof AustraliaPayidAccount) { new Popup().information(Res.get("payment.payid.info", currencyName, currencyName)) .width(900) .closeButtonText(Res.get("shared.cancel")) @@ -361,6 +382,16 @@ public class FiatAccountsView extends PaymentAccountsView list = PaymentMethod.getPaymentMethods().stream() - .filter(paymentMethod -> !paymentMethod.isAsset()) + .filter(PaymentMethod::isFiat) .sorted() .collect(Collectors.toList()); paymentMethodComboBox.setItems(FXCollections.observableArrayList(list)); @@ -420,6 +451,12 @@ public class FiatAccountsView extends PaymentAccountsView tuple2 = add2ButtonsAfterGroup(root, ++gridRow, Res.get("shared.saveNewAccount"), Res.get("shared.cancel")); @@ -435,21 +472,32 @@ public class FiatAccountsView extends PaymentAccountsView tuple = add2ButtonsAfterGroup(root, ++gridRow, Res.get("shared.deleteAccount"), Res.get("shared.cancel")); - Button deleteAccountButton = tuple.first; + Tuple3 tuple = add3ButtonsAfterGroup( + root, + ++gridRow, + Res.get("shared.save"), + Res.get("shared.deleteAccount"), + Res.get("shared.cancel") + ); + Button updateButton = tuple.first; + updateButton.setOnAction(event -> onUpdateAccount(paymentMethodForm.getPaymentAccount())); + Button deleteAccountButton = tuple.second; deleteAccountButton.setOnAction(event -> onDeleteAccount(paymentMethodForm.getPaymentAccount())); - Button cancelButton = tuple.second; - cancelButton.setOnAction(event -> removeSelectAccountForm()); + Button cancelButton = tuple.third; + cancelButton.setOnAction(event -> onCancelSelectedAccount(paymentMethodForm.getPaymentAccount())); GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan()); - model.onSelectAccount(paymentAccount); + model.onSelectAccount(current); } } @@ -481,9 +529,9 @@ public class FiatAccountsView extends PaymentAccountsView { .append("Market: ").append(CurrencyUtil.getCurrencyPair(tradeStatistics3.getCurrency())).append("\n") .append("Price: ").append(FormattingUtils.formatPrice(tradeStatistics3.getTradePrice())).append("\n") .append("Amount: ").append(formatter.formatCoin(tradeStatistics3.getTradeAmount())).append("\n") - .append("Volume: ").append(DisplayUtils.formatVolume(tradeStatistics3.getTradeVolume())).append("\n") - .append("Payment method: ").append(Res.get(tradeStatistics3.getPaymentMethod())).append("\n") + .append("Volume: ").append(VolumeUtil.formatVolume(tradeStatistics3.getTradeVolume())).append("\n") + .append("Payment method: ").append(Res.get(tradeStatistics3.getPaymentMethodId())).append("\n") .append("ReferralID: ").append(tradeStatistics3.getExtraDataMap().get(OfferPayload.REFERRAL_ID)); return sb.toString(); }) @@ -210,8 +211,8 @@ public class MarketView extends ActivatableView { private String getAllOffersWithReferralId() { List list = offerBook.getOfferBookListItems().stream() .map(OfferBookListItem::getOffer) - .filter(offer -> offer.getOfferPayload().getExtraDataMap() != null) - .filter(offer -> offer.getOfferPayload().getExtraDataMap().get(OfferPayload.REFERRAL_ID) != null) + .filter(offer -> offer.getExtraDataMap() != null) + .filter(offer -> offer.getExtraDataMap().get(OfferPayload.REFERRAL_ID) != null) .map(offer -> { StringBuilder sb = new StringBuilder(); sb.append("Offer ID: ").append(offer.getId()).append("\n") @@ -220,7 +221,7 @@ public class MarketView extends ActivatableView { .append("Price: ").append(FormattingUtils.formatPrice(offer.getPrice())).append("\n") .append("Amount: ").append(DisplayUtils.formatAmount(offer, formatter)).append(" BTC\n") .append("Payment method: ").append(Res.get(offer.getPaymentMethod().getId())).append("\n") - .append("ReferralID: ").append(offer.getOfferPayload().getExtraDataMap().get(OfferPayload.REFERRAL_ID)); + .append("ReferralID: ").append(offer.getExtraDataMap().get(OfferPayload.REFERRAL_ID)); return sb.toString(); }) .collect(Collectors.toList()); diff --git a/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartView.java b/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartView.java index c3f34022d0..aa771e4de3 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartView.java @@ -17,7 +17,6 @@ package bisq.desktop.main.market.offerbook; -import bisq.desktop.Navigation; import bisq.desktop.common.view.ActivatableViewAndModel; import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.AutoTooltipButton; @@ -26,9 +25,6 @@ import bisq.desktop.components.AutoTooltipTableColumn; import bisq.desktop.components.AutocompleteComboBox; import bisq.desktop.components.ColoredDecimalPlacesWithZerosText; import bisq.desktop.components.PeerInfoIconSmall; -import bisq.desktop.main.MainView; -import bisq.desktop.main.offer.BuyOfferView; -import bisq.desktop.main.offer.SellOfferView; import bisq.desktop.main.offer.offerbook.OfferBookListItem; import bisq.desktop.util.CurrencyListItem; import bisq.desktop.util.DisplayUtils; @@ -37,8 +33,9 @@ import bisq.desktop.util.GUIUtil; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferDirection; import bisq.core.util.FormattingUtils; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.CoinFormatter; import bisq.network.p2p.NodeAddress; @@ -84,15 +81,13 @@ import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; - -import java.text.DecimalFormat; import javafx.util.Callback; import javafx.util.StringConverter; +import java.text.DecimalFormat; + import java.util.List; import java.util.concurrent.TimeUnit; import java.util.function.Function; @@ -107,7 +102,6 @@ public class OfferBookChartView extends ActivatableViewAndModel seriesBuy, seriesSell; - private final Navigation navigation; private final CoinFormatter formatter; private TableView buyOfferTableView; private TableView sellOfferTableView; @@ -117,19 +111,19 @@ public class OfferBookChartView extends ActivatableViewAndModel selectedTabIndexListener; private SingleSelectionModel tabPaneSelectionModel; - private Label leftHeaderLabel, rightHeaderLabel; + private Label sellHeaderLabel, buyHeaderLabel; private ChangeListener sellTableRowSelectionListener, buyTableRowSelectionListener; - private HBox bottomHBox; private ListChangeListener changeListener; private ListChangeListener currencyListItemsListener; private final double dataLimitFactor = 3; private final double initialOfferTableViewHeight = 121; - private final double pixelsPerOfferTableRow = (initialOfferTableViewHeight - 30) / 5.0; // initial visible row count=5, header height=30 private final Function offerTableViewHeight = (screenSize) -> { + // initial visible row count=5, header height=30 + double pixelsPerOfferTableRow = (initialOfferTableViewHeight - 30) / 5.0; int extraRows = screenSize <= INITIAL_WINDOW_HEIGHT ? 0 : (int) ((screenSize - INITIAL_WINDOW_HEIGHT) / pixelsPerOfferTableRow); return extraRows == 0 ? initialOfferTableViewHeight : Math.ceil(initialOfferTableViewHeight + ((extraRows + 1) * pixelsPerOfferTableRow)); }; @@ -140,10 +134,10 @@ public class OfferBookChartView extends ActivatableViewAndModel, VBox, Button, Label> tupleBuy = getOfferTable(OfferPayload.Direction.BUY); - Tuple4, VBox, Button, Label> tupleSell = getOfferTable(OfferPayload.Direction.SELL); + Tuple4, VBox, Button, Label> tupleBuy = getOfferTable(OfferDirection.BUY); + Tuple4, VBox, Button, Label> tupleSell = getOfferTable(OfferDirection.SELL); buyOfferTableView = tupleBuy.first; sellOfferTableView = tupleSell.first; - leftButton = (AutoTooltipButton) tupleBuy.third; - rightButton = (AutoTooltipButton) tupleSell.third; + sellButton = (AutoTooltipButton) tupleBuy.third; + buyButton = (AutoTooltipButton) tupleSell.third; - leftHeaderLabel = tupleBuy.fourth; - rightHeaderLabel = tupleSell.fourth; + sellHeaderLabel = tupleBuy.fourth; + buyHeaderLabel = tupleSell.fourth; - bottomHBox = new HBox(); + HBox bottomHBox = new HBox(); bottomHBox.setSpacing(20); //30 bottomHBox.setAlignment(Pos.CENTER); VBox.setMargin(bottomHBox, new Insets(-5, 0, 0, 0)); HBox.setHgrow(tupleBuy.second, Priority.ALWAYS); HBox.setHgrow(tupleSell.second, Priority.ALWAYS); - tupleBuy.second.setUserData(OfferPayload.Direction.BUY.name()); - tupleSell.second.setUserData(OfferPayload.Direction.SELL.name()); + tupleBuy.second.setUserData(OfferDirection.BUY.name()); + tupleSell.second.setUserData(OfferDirection.SELL.name()); bottomHBox.getChildren().addAll(tupleBuy.second, tupleSell.second); root.getChildren().addAll(currencyComboBoxTuple.first, chartPane, bottomHBox); @@ -222,15 +216,15 @@ public class OfferBookChartView extends ActivatableViewAndModel() { - int cryptoPrecision = 3; - DecimalFormat df = new DecimalFormat(",###"); + final int cryptoPrecision = 3; + final DecimalFormat df = new DecimalFormat(",###"); @Override public String toString(Number object) { final double doubleValue = (double) object; if (CurrencyUtil.isCryptoCurrency(model.getCurrencyCode())) { final String withCryptoPrecision = FormattingUtils.formatRoundedDoubleWithPrecision(doubleValue, cryptoPrecision); - if (withCryptoPrecision.substring(0,3).equals("0.0")) { + if (withCryptoPrecision.startsWith("0.0")) { return FormattingUtils.formatRoundedDoubleWithPrecision(doubleValue, 8).replaceFirst("0+$", ""); } else { return withCryptoPrecision.replaceFirst("0+$", ""); @@ -246,37 +240,21 @@ public class OfferBookChartView extends ActivatableViewAndModel { - private ComboBox comboBox; + private final ComboBox comboBox; CurrencyListItemStringConverter(ComboBox comboBox) { this.comboBox = comboBox; @@ -319,14 +297,8 @@ public class OfferBookChartView extends ActivatableViewAndModel { - model.preferences.setSellScreenCurrencyCode(model.getCurrencyCode()); - navigation.navigateTo(MainView.class, SellOfferView.class); - }; - sellTableRowSelectionListener = (observable, oldValue, newValue) -> { - model.preferences.setBuyScreenCurrencyCode(model.getCurrencyCode()); - navigation.navigateTo(MainView.class, BuyOfferView.class); - }; + buyTableRowSelectionListener = (observable, oldValue, newValue) -> model.goToOfferView(OfferDirection.BUY); + sellTableRowSelectionListener = (observable, oldValue, newValue) -> model.goToOfferView(OfferDirection.SELL); bisqWindowVerticalSizeListener = (observable, oldValue, newValue) -> layout(); } @@ -387,78 +359,75 @@ public class OfferBookChartView extends ActivatableViewAndModel> filterOutliersBuy(List> buy, boolean isCrypto) { - List mnmx = isCrypto ? minMaxFilterRight(buy) : minMaxFilterLeft(buy); - if (mnmx.get(0).doubleValue() == Double.MAX_VALUE || - mnmx.get(1).doubleValue() == Double.MIN_VALUE) { // no filtering + List> filterOutliersBuy(List> buy) { + List mnmx = minMaxFilterLeft(buy); + if (mnmx.get(0) == Double.MAX_VALUE || + mnmx.get(1) == Double.MIN_VALUE) { // no filtering return buy; } // apply filtering - return isCrypto ? filterRight(buy, mnmx.get(0)) : filterLeft(buy, mnmx.get(1)); + return filterLeft(buy, mnmx.get(1)); } - List> filterOutliersSell(List> sell, boolean isCrypto) { - List mnmx = isCrypto ? minMaxFilterLeft(sell) : minMaxFilterRight(sell); - if (mnmx.get(0).doubleValue() == Double.MAX_VALUE || - mnmx.get(1).doubleValue() == Double.MIN_VALUE) { // no filtering + List> filterOutliersSell(List> sell) { + List mnmx = minMaxFilterRight(sell); + if (mnmx.get(0) == Double.MAX_VALUE || + mnmx.get(1) == Double.MIN_VALUE) { // no filtering return sell; } // apply filtering - return isCrypto ? filterLeft(sell, mnmx.get(1)) : filterRight(sell, mnmx.get(0)); + return filterRight(sell, mnmx.get(0)); } private List minMaxFilterLeft(List> data) { double maxValue = data.stream() - .mapToDouble(o -> o.getXValue().doubleValue()) - .max() - .orElse(Double.MIN_VALUE); + .mapToDouble(o -> o.getXValue().doubleValue()) + .max() + .orElse(Double.MIN_VALUE); // Hide offers less than a div-factor of dataLimitFactor lower than the highest offer. double minValue = data.stream() - .mapToDouble(o -> o.getXValue().doubleValue()) - .filter(o -> o > maxValue / dataLimitFactor) - .min() - .orElse(Double.MAX_VALUE); + .mapToDouble(o -> o.getXValue().doubleValue()) + .filter(o -> o > maxValue / dataLimitFactor) + .min() + .orElse(Double.MAX_VALUE); return List.of(minValue, maxValue); } private List minMaxFilterRight(List> data) { double minValue = data.stream() - .mapToDouble(o -> o.getXValue().doubleValue()) - .min() - .orElse(Double.MAX_VALUE); + .mapToDouble(o -> o.getXValue().doubleValue()) + .min() + .orElse(Double.MAX_VALUE); // Hide offers a dataLimitFactor factor higher than the lowest offer double maxValue = data.stream() - .mapToDouble(o -> o.getXValue().doubleValue()) - .filter(o -> o < minValue * dataLimitFactor) - .max() - .orElse(Double.MIN_VALUE); + .mapToDouble(o -> o.getXValue().doubleValue()) + .filter(o -> o < minValue * dataLimitFactor) + .max() + .orElse(Double.MIN_VALUE); return List.of(minValue, maxValue); } private List> filterLeft(List> data, double maxValue) { return data.stream() - .filter(o -> o.getXValue().doubleValue() > maxValue / dataLimitFactor) - .collect(Collectors.toList()); + .filter(o -> o.getXValue().doubleValue() > maxValue / dataLimitFactor) + .collect(Collectors.toList()); } private List> filterRight(List> data, double minValue) { return data.stream() - .filter(o -> o.getXValue().doubleValue() < minValue * dataLimitFactor) - .collect(Collectors.toList()); + .filter(o -> o.getXValue().doubleValue() < minValue * dataLimitFactor) + .collect(Collectors.toList()); } - private Tuple4, VBox, Button, Label> getOfferTable(OfferPayload.Direction direction) { + private Tuple4, VBox, Button, Label> getOfferTable(OfferDirection direction) { TableView tableView = new TableView<>(); tableView.setMinHeight(initialOfferTableViewHeight); tableView.setPrefHeight(initialOfferTableViewHeight); @@ -481,7 +450,9 @@ public class OfferBookChartView extends ActivatableViewAndModel listener = new ChangeListener<>() { @Override - public void changed(ObservableValue observable, Number oldValue, Number newValue) { + public void changed(ObservableValue observable, + Number oldValue, + Number newValue) { if (offer != null && offer.getPrice() != null) { setText(""); setGraphic(new ColoredDecimalPlacesWithZerosText(model.getPrice(offer), @@ -531,7 +502,9 @@ public class OfferBookChartView extends ActivatableViewAndModel listener = new ChangeListener<>() { @Override - public void changed(ObservableValue observable, Number oldValue, Number newValue) { + public void changed(ObservableValue observable, + Number oldValue, + Number newValue) { if (offer != null && offer.getPrice() != null) { renderCellContentRange(); model.priceFeedService.updateCounterProperty().removeListener(listener); @@ -564,7 +537,7 @@ public class OfferBookChartView extends ActivatableViewAndModel avatarColumn = new AutoTooltipTableColumn<>(isSellOffer ? @@ -625,16 +598,21 @@ public class OfferBookChartView extends ActivatableViewAndModel { - if (isSellOffer) { - model.preferences.setBuyScreenCurrencyCode(model.getCurrencyCode()); - navigation.navigateTo(MainView.class, BuyOfferView.class); - } else { - model.preferences.setSellScreenCurrencyCode(model.getCurrencyCode()); - navigation.navigateTo(MainView.class, SellOfferView.class); - } - }); + button.setOnAction(e -> model.goToOfferView(direction)); Region spacer = new Region(); @@ -694,18 +664,6 @@ public class OfferBookChartView extends ActivatableViewAndModel(tableView, vBox, button, titleLabel); } - private void reverseTableColumns() { - ObservableList> columns = FXCollections.observableArrayList(buyOfferTableView.getColumns()); - buyOfferTableView.getColumns().clear(); - FXCollections.reverse(columns); - buyOfferTableView.getColumns().addAll(columns); - - columns = FXCollections.observableArrayList(sellOfferTableView.getColumns()); - sellOfferTableView.getColumns().clear(); - FXCollections.reverse(columns); - sellOfferTableView.getColumns().addAll(columns); - } - private void layout() { UserThread.runAfter(() -> { if (root.getScene() != null) { @@ -713,8 +671,8 @@ public class OfferBookChartView extends ActivatableViewAndModel e.getOfferPayload().getCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()))) + .anyMatch(e -> e.getCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()))) updateChartData(); } @@ -193,11 +205,24 @@ class OfferBookChartViewModel extends ActivatableViewModel { } } - void setSelectedTabIndex(int selectedTabIndex) { + public void setSelectedTabIndex(int selectedTabIndex) { this.selectedTabIndex = selectedTabIndex; syncPriceFeedCurrency(); } + public boolean isSellOffer(OfferDirection direction) { + return direction == OfferDirection.SELL; + } + + public boolean isMyOffer(Offer offer) { + return openOfferManager.isMyOffer(offer); + } + + public void goToOfferView(OfferDirection direction) { + updateScreenCurrencyInPreferences(direction); + Class offerView = isSellOffer(direction) ? BuyOfferView.class : SellOfferView.class; + navigation.navigateTo(MainView.class, offerView, OfferViewUtil.getOfferBookViewClass(getCurrencyCode())); + } /////////////////////////////////////////////////////////////////////////////////////////// // Getters @@ -232,15 +257,20 @@ class OfferBookChartViewModel extends ActivatableViewModel { } public Optional getSelectedCurrencyListItem() { - return currencyListItems.getObservableList().stream().filter(e -> e.tradeCurrency.equals(selectedTradeCurrencyProperty.get())).findAny(); + return currencyListItems.getObservableList().stream() + .filter(e -> e.tradeCurrency.equals(selectedTradeCurrencyProperty.get())).findAny(); } public int getMaxNumberOfPriceZeroDecimalsToColorize(Offer offer) { - return CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) ? GUIUtil.FIAT_DECIMALS_WITH_ZEROS : GUIUtil.ALTCOINS_DECIMALS_WITH_ZEROS; + return offer.isFiatOffer() + ? GUIUtil.FIAT_DECIMALS_WITH_ZEROS + : GUIUtil.ALTCOINS_DECIMALS_WITH_ZEROS; } public int getZeroDecimalsForPrice(Offer offer) { - return CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) ? GUIUtil.FIAT_PRICE_DECIMALS_WITH_ZEROS : GUIUtil.ALTCOINS_DECIMALS_WITH_ZEROS; + return offer.isFiatOffer() + ? GUIUtil.FIAT_PRICE_DECIMALS_WITH_ZEROS + : GUIUtil.ALTCOINS_DECIMALS_WITH_ZEROS; } public String getPrice(Offer offer) { @@ -248,7 +278,9 @@ class OfferBookChartViewModel extends ActivatableViewModel { } private String formatPrice(Offer offer, boolean decimalAligned) { - return DisplayUtils.formatPrice(offer.getPrice(), decimalAligned, offer.isBuyOffer() ? maxPlacesForBuyPrice.get() : maxPlacesForSellPrice.get()); + return DisplayUtils.formatPrice(offer.getPrice(), decimalAligned, offer.isBuyOffer() + ? maxPlacesForBuyPrice.get() + : maxPlacesForSellPrice.get()); } public String getVolume(Offer offer) { @@ -256,7 +288,10 @@ class OfferBookChartViewModel extends ActivatableViewModel { } private String formatVolume(Offer offer, boolean decimalAligned) { - return DisplayUtils.formatVolume(offer, decimalAligned, offer.isBuyOffer() ? maxPlacesForBuyVolume.get() : maxPlacesForSellVolume.get(), false); + return VolumeUtil.formatVolume(offer, + decimalAligned, + offer.isBuyOffer() ? maxPlacesForBuyVolume.get() : maxPlacesForSellVolume.get(), + false); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -282,7 +317,7 @@ class OfferBookChartViewModel extends ActivatableViewModel { // the buy column is actually the sell column and vice versa. To maintain the expected // ordering, we have to reverse the price comparator. boolean isCrypto = CurrencyUtil.isCryptoCurrency(getCurrencyCode()); - if (isCrypto) offerPriceComparator = offerPriceComparator.reversed(); +// if (isCrypto) offerPriceComparator = offerPriceComparator.reversed(); // Offer amounts are used for the secondary sort. They are sorted from high to low. Comparator offerAmountComparator = Comparator.comparing(Offer::getAmount).reversed(); @@ -294,10 +329,12 @@ class OfferBookChartViewModel extends ActivatableViewModel { offerPriceComparator .thenComparing(offerAmountComparator); + OfferDirection buyOfferDirection = isCrypto ? OfferDirection.SELL : OfferDirection.BUY; + List allBuyOffers = offerBookListItems.stream() .map(OfferBookListItem::getOffer) .filter(e -> e.getCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) - && e.getDirection().equals(OfferPayload.Direction.BUY)) + && e.getDirection().equals(buyOfferDirection)) .sorted(buyOfferSortComparator) .collect(Collectors.toList()); @@ -321,12 +358,14 @@ class OfferBookChartViewModel extends ActivatableViewModel { maxPlacesForBuyVolume.set(formatVolume(offer, false).length()); } - buildChartAndTableEntries(allBuyOffers, OfferPayload.Direction.BUY, buyData, topBuyOfferList); + buildChartAndTableEntries(allBuyOffers, OfferDirection.BUY, buyData, topBuyOfferList); + + OfferDirection sellOfferDirection = isCrypto ? OfferDirection.BUY : OfferDirection.SELL; List allSellOffers = offerBookListItems.stream() .map(OfferBookListItem::getOffer) .filter(e -> e.getCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) - && e.getDirection().equals(OfferPayload.Direction.SELL)) + && e.getDirection().equals(sellOfferDirection)) .sorted(sellOfferSortComparator) .collect(Collectors.toList()); @@ -348,11 +387,11 @@ class OfferBookChartViewModel extends ActivatableViewModel { maxPlacesForSellVolume.set(formatVolume(offer, false).length()); } - buildChartAndTableEntries(allSellOffers, OfferPayload.Direction.SELL, sellData, topSellOfferList); + buildChartAndTableEntries(allSellOffers, OfferDirection.SELL, sellData, topSellOfferList); } private void buildChartAndTableEntries(List sortedList, - OfferPayload.Direction direction, + OfferDirection direction, List> data, ObservableList offerTableList) { data.clear(); @@ -366,17 +405,10 @@ class OfferBookChartViewModel extends ActivatableViewModel { offerTableListTemp.add(new OfferListItem(offer, accumulatedAmount)); double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent()); - if (CurrencyUtil.isCryptoCurrency(getCurrencyCode())) { - if (direction.equals(OfferPayload.Direction.SELL)) - data.add(0, new XYChart.Data<>(priceAsDouble, accumulatedAmount)); - else - data.add(new XYChart.Data<>(priceAsDouble, accumulatedAmount)); - } else { - if (direction.equals(OfferPayload.Direction.BUY)) - data.add(0, new XYChart.Data<>(priceAsDouble, accumulatedAmount)); - else - data.add(new XYChart.Data<>(priceAsDouble, accumulatedAmount)); - } + if (direction.equals(OfferDirection.BUY)) + data.add(0, new XYChart.Data<>(priceAsDouble, accumulatedAmount)); + else + data.add(new XYChart.Data<>(priceAsDouble, accumulatedAmount)); } } offerTableList.setAll(offerTableListTemp); @@ -385,4 +417,20 @@ class OfferBookChartViewModel extends ActivatableViewModel { private boolean isEditEntry(String id) { return id.equals(GUIUtil.EDIT_FLAG); } + + private void updateScreenCurrencyInPreferences(OfferDirection direction) { + if (isSellOffer(direction)) { + if (CurrencyUtil.isFiatCurrency(getCurrencyCode())) { + preferences.setBuyScreenCurrencyCode(getCurrencyCode()); + } else if (!getCurrencyCode().equals(GUIUtil.TOP_ALTCOIN.getCode())) { + preferences.setBuyScreenCryptoCurrencyCode(getCurrencyCode()); + } + } else { + if (CurrencyUtil.isFiatCurrency(getCurrencyCode())) { + preferences.setSellScreenCurrencyCode(getCurrencyCode()); + } else if (!getCurrencyCode().equals(GUIUtil.TOP_ALTCOIN.getCode())) { + preferences.setSellScreenCryptoCurrencyCode(getCurrencyCode()); + } + } + } } diff --git a/desktop/src/main/java/bisq/desktop/main/market/spread/SpreadViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/spread/SpreadViewModel.java index f73c74e564..c03ec94a03 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/spread/SpreadViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/spread/SpreadViewModel.java @@ -27,7 +27,7 @@ import bisq.core.locale.Res; import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferDirection; import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.PriceFeedService; import bisq.core.util.FormattingUtils; @@ -139,13 +139,13 @@ class SpreadViewModel extends ActivatableViewModel { for (String key : offersByCurrencyMap.keySet()) { List offers = offersByCurrencyMap.get(key); - final boolean isFiatCurrency = (offers.size() > 0 && !offers.get(0).getPaymentMethod().isAsset()); + boolean isFiatCurrency = (offers.size() > 0 && offers.get(0).getPaymentMethod().isFiat()); List uniqueOffers = offers.stream().filter(distinctByKey(Offer::getId)).collect(Collectors.toList()); List buyOffers = uniqueOffers .stream() - .filter(e -> e.getDirection().equals(OfferPayload.Direction.BUY)) + .filter(e -> e.getDirection().equals(OfferDirection.BUY)) .sorted((o1, o2) -> { long a = o1.getPrice() != null ? o1.getPrice().getValue() : 0; long b = o2.getPrice() != null ? o2.getPrice().getValue() : 0; @@ -162,7 +162,7 @@ class SpreadViewModel extends ActivatableViewModel { List sellOffers = uniqueOffers .stream() - .filter(e -> e.getDirection().equals(OfferPayload.Direction.SELL)) + .filter(e -> e.getDirection().equals(OfferDirection.SELL)) .sorted((o1, o2) -> { long a = o1.getPrice() != null ? o1.getPrice().getValue() : 0; long b = o2.getPrice() != null ? o2.getPrice().getValue() : 0; diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java new file mode 100644 index 0000000000..526e733dfd --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java @@ -0,0 +1,303 @@ +/* + * 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 . + */ + +package bisq.desktop.main.market.trades; + +import bisq.desktop.main.market.trades.charts.CandleData; +import bisq.desktop.util.DisplayUtils; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.monetary.Altcoin; +import bisq.core.trade.statistics.TradeStatistics3; + +import bisq.common.util.MathUtils; + +import org.bitcoinj.core.Coin; + +import com.google.common.annotations.VisibleForTesting; + +import javafx.scene.chart.XYChart; + +import javafx.util.Pair; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import lombok.Getter; + +import static bisq.desktop.main.market.trades.TradesChartsViewModel.MAX_TICKS; + +public class ChartCalculations { + static final ZoneId ZONE_ID = ZoneId.systemDefault(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Async + /////////////////////////////////////////////////////////////////////////////////////////// + + static CompletableFuture>> getUsdAveragePriceMapsPerTickUnit(Set tradeStatisticsSet) { + return CompletableFuture.supplyAsync(() -> { + Map> usdAveragePriceMapsPerTickUnit = new HashMap<>(); + Map>> dateMapsPerTickUnit = new HashMap<>(); + for (TradesChartsViewModel.TickUnit tick : TradesChartsViewModel.TickUnit.values()) { + dateMapsPerTickUnit.put(tick, new HashMap<>()); + } + + tradeStatisticsSet.stream() + .filter(e -> e.getCurrency().equals("USD")) + .forEach(tradeStatistics -> { + for (TradesChartsViewModel.TickUnit tick : TradesChartsViewModel.TickUnit.values()) { + long time = roundToTick(tradeStatistics.getLocalDateTime(), tick).getTime(); + Map> map = dateMapsPerTickUnit.get(tick); + map.putIfAbsent(time, new ArrayList<>()); + map.get(time).add(tradeStatistics); + } + }); + + dateMapsPerTickUnit.forEach((tick, map) -> { + HashMap priceMap = new HashMap<>(); + map.forEach((date, tradeStatisticsList) -> priceMap.put(date, getAveragePrice(tradeStatisticsList))); + usdAveragePriceMapsPerTickUnit.put(tick, priceMap); + }); + return usdAveragePriceMapsPerTickUnit; + }); + } + + static CompletableFuture> getTradeStatisticsForCurrency(Set tradeStatisticsSet, + String currencyCode, + boolean showAllTradeCurrencies) { + return CompletableFuture.supplyAsync(() -> { + return tradeStatisticsSet.stream() + .filter(e -> showAllTradeCurrencies || e.getCurrency().equals(currencyCode)) + .collect(Collectors.toList()); + }); + } + + static CompletableFuture getUpdateChartResult(List tradeStatisticsByCurrency, + TradesChartsViewModel.TickUnit tickUnit, + Map> usdAveragePriceMapsPerTickUnit, + String currencyCode) { + return CompletableFuture.supplyAsync(() -> { + // Generate date range and create sets for all ticks + Map>> itemsPerInterval = getItemsPerInterval(tradeStatisticsByCurrency, tickUnit); + + Map usdAveragePriceMap = usdAveragePriceMapsPerTickUnit.get(tickUnit); + AtomicLong averageUsdPrice = new AtomicLong(0); + + // create CandleData for defined time interval + List candleDataList = itemsPerInterval.entrySet().stream() + .filter(entry -> entry.getKey() >= 0 && !entry.getValue().getValue().isEmpty()) + .map(entry -> { + long tickStartDate = entry.getValue().getKey().getTime(); + // If we don't have a price we take the previous one + if (usdAveragePriceMap.containsKey(tickStartDate)) { + averageUsdPrice.set(usdAveragePriceMap.get(tickStartDate)); + } + return getCandleData(entry.getKey(), entry.getValue().getValue(), averageUsdPrice.get(), tickUnit, currencyCode, itemsPerInterval); + }) + .sorted(Comparator.comparingLong(o -> o.tick)) + .collect(Collectors.toList()); + + List> priceItems = candleDataList.stream() + .map(e -> new XYChart.Data(e.tick, e.open, e)) + .collect(Collectors.toList()); + + List> volumeItems = candleDataList.stream() + .map(candleData -> new XYChart.Data(candleData.tick, candleData.accumulatedAmount, candleData)) + .collect(Collectors.toList()); + + List> volumeInUsdItems = candleDataList.stream() + .map(candleData -> new XYChart.Data(candleData.tick, candleData.volumeInUsd, candleData)) + .collect(Collectors.toList()); + + return new UpdateChartResult(itemsPerInterval, priceItems, volumeItems, volumeInUsdItems); + }); + } + + @Getter + static class UpdateChartResult { + private final Map>> itemsPerInterval; + private final List> priceItems; + private final List> volumeItems; + private final List> volumeInUsdItems; + + public UpdateChartResult(Map>> itemsPerInterval, + List> priceItems, + List> volumeItems, + List> volumeInUsdItems) { + + this.itemsPerInterval = itemsPerInterval; + this.priceItems = priceItems; + this.volumeItems = volumeItems; + this.volumeInUsdItems = volumeInUsdItems; + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + static Map>> getItemsPerInterval(List tradeStatisticsByCurrency, + TradesChartsViewModel.TickUnit tickUnit) { + // Generate date range and create sets for all ticks + Map>> itemsPerInterval = new HashMap<>(); + Date time = new Date(); + for (long i = MAX_TICKS + 1; i >= 0; --i) { + Pair> pair = new Pair<>((Date) time.clone(), new HashSet<>()); + itemsPerInterval.put(i, pair); + // We adjust the time for the next iteration + time.setTime(time.getTime() - 1); + time = roundToTick(time, tickUnit); + } + + // Get all entries for the defined time interval + tradeStatisticsByCurrency.forEach(tradeStatistics -> { + for (long i = MAX_TICKS; i > 0; --i) { + Pair> pair = itemsPerInterval.get(i); + if (tradeStatistics.getDate().after(pair.getKey())) { + pair.getValue().add(tradeStatistics); + break; + } + } + }); + return itemsPerInterval; + } + + + static Date roundToTick(LocalDateTime localDate, TradesChartsViewModel.TickUnit tickUnit) { + switch (tickUnit) { + case YEAR: + return Date.from(localDate.withMonth(1).withDayOfYear(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); + case MONTH: + return Date.from(localDate.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); + case WEEK: + int dayOfWeek = localDate.getDayOfWeek().getValue(); + LocalDateTime firstDayOfWeek = ChronoUnit.DAYS.addTo(localDate, 1 - dayOfWeek); + return Date.from(firstDayOfWeek.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); + case DAY: + return Date.from(localDate.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); + case HOUR: + return Date.from(localDate.withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); + case MINUTE_10: + return Date.from(localDate.withMinute(localDate.getMinute() - localDate.getMinute() % 10).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); + default: + return Date.from(localDate.atZone(ZONE_ID).toInstant()); + } + } + + static Date roundToTick(Date time, TradesChartsViewModel.TickUnit tickUnit) { + return roundToTick(time.toInstant().atZone(ChartCalculations.ZONE_ID).toLocalDateTime(), tickUnit); + } + + private static long getAveragePrice(List tradeStatisticsList) { + long accumulatedAmount = 0; + long accumulatedVolume = 0; + for (TradeStatistics3 tradeStatistics : tradeStatisticsList) { + accumulatedAmount += tradeStatistics.getAmount(); + accumulatedVolume += tradeStatistics.getTradeVolume().getValue(); + } + + double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT); + return MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / accumulatedAmount); + } + + @VisibleForTesting + static CandleData getCandleData(long tick, Set set, + long averageUsdPrice, + TradesChartsViewModel.TickUnit tickUnit, + String currencyCode, + Map>> itemsPerInterval) { + long open = 0; + long close = 0; + long high = 0; + long low = 0; + long accumulatedVolume = 0; + long accumulatedAmount = 0; + long numTrades = set.size(); + List tradePrices = new ArrayList<>(); + for (TradeStatistics3 item : set) { + long tradePriceAsLong = item.getTradePrice().getValue(); + // Previously a check was done which inverted the low and high for cryptocurrencies. + low = (low != 0) ? Math.min(low, tradePriceAsLong) : tradePriceAsLong; + high = (high != 0) ? Math.max(high, tradePriceAsLong) : tradePriceAsLong; + + accumulatedVolume += item.getTradeVolume().getValue(); + accumulatedAmount += item.getTradeAmount().getValue(); + tradePrices.add(tradePriceAsLong); + } + Collections.sort(tradePrices); + + List list = new ArrayList<>(set); + list.sort(Comparator.comparingLong(TradeStatistics3::getDateAsLong)); + if (list.size() > 0) { + open = list.get(0).getTradePrice().getValue(); + close = list.get(list.size() - 1).getTradePrice().getValue(); + } + + long averagePrice; + Long[] prices = new Long[tradePrices.size()]; + tradePrices.toArray(prices); + long medianPrice = MathUtils.getMedian(prices); + boolean isBullish; + if (CurrencyUtil.isCryptoCurrency(currencyCode)) { + isBullish = close < open; + double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, Altcoin.SMALLEST_UNIT_EXPONENT); + averagePrice = MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / accumulatedVolume); + } else { + isBullish = close > open; + double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT); + averagePrice = MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / accumulatedAmount); + } + + Date dateFrom = new Date(getTimeFromTickIndex(tick, itemsPerInterval)); + Date dateTo = new Date(getTimeFromTickIndex(tick + 1, itemsPerInterval)); + String dateString = tickUnit.ordinal() > TradesChartsViewModel.TickUnit.DAY.ordinal() ? + DisplayUtils.formatDateTimeSpan(dateFrom, dateTo) : + DisplayUtils.formatDate(dateFrom) + " - " + DisplayUtils.formatDate(dateTo); + + // We do not need precision, so we scale down before multiplication otherwise we could get an overflow. + averageUsdPrice = (long) MathUtils.scaleDownByPowerOf10((double) averageUsdPrice, 4); + long volumeInUsd = averageUsdPrice * (long) MathUtils.scaleDownByPowerOf10((double) accumulatedAmount, 4); + // We store USD value without decimals as its only total volume, no precision is needed. + volumeInUsd = (long) MathUtils.scaleDownByPowerOf10((double) volumeInUsd, 4); + return new CandleData(tick, open, close, high, low, averagePrice, medianPrice, accumulatedAmount, accumulatedVolume, + numTrades, isBullish, dateString, volumeInUsd); + } + + static long getTimeFromTickIndex(long tick, Map>> itemsPerInterval) { + if (tick > MAX_TICKS + 1 || + itemsPerInterval.get(tick) == null) { + return 0; + } + return itemsPerInterval.get(tick).getKey().getTime(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradeStatistics3ListItem.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradeStatistics3ListItem.java index 842767db8e..a58a132b17 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradeStatistics3ListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradeStatistics3ListItem.java @@ -23,6 +23,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.trade.statistics.TradeStatistics3; import bisq.core.util.FormattingUtils; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.CoinFormatter; import lombok.experimental.Delegate; @@ -73,8 +74,8 @@ public class TradeStatistics3ListItem { public String getVolumeString() { if (volumeString == null) { volumeString = tradeStatistics3 != null ? showAllTradeCurrencies ? - DisplayUtils.formatVolumeWithCode(tradeStatistics3.getTradeVolume()) : - DisplayUtils.formatVolume(tradeStatistics3.getTradeVolume()) + VolumeUtil.formatVolumeWithCode(tradeStatistics3.getTradeVolume()) : + VolumeUtil.formatVolume(tradeStatistics3.getTradeVolume()) : ""; } return volumeString; @@ -82,7 +83,7 @@ public class TradeStatistics3ListItem { public String getPaymentMethodString() { if (paymentMethodString == null) { - paymentMethodString = tradeStatistics3 != null ? Res.get(tradeStatistics3.getPaymentMethod()) : ""; + paymentMethodString = tradeStatistics3 != null ? Res.get(tradeStatistics3.getPaymentMethodId()) : ""; } return paymentMethodString; } diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java index 19f5f2abe8..495cfe538f 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java @@ -38,6 +38,7 @@ import bisq.core.trade.statistics.TradeStatistics3; import bisq.core.user.CookieKey; import bisq.core.user.User; import bisq.core.util.FormattingUtils; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.CoinFormatter; import bisq.common.UserThread; @@ -102,11 +103,12 @@ import java.text.DecimalFormat; import java.util.Comparator; import java.util.Date; import java.util.List; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; +import static bisq.desktop.main.market.trades.TradesChartsViewModel.MAX_TICKS; import static bisq.desktop.util.FormBuilder.addTopLabelAutocompleteComboBox; import static bisq.desktop.util.FormBuilder.getTopLabelWithVBox; @@ -160,8 +162,7 @@ public class TradesChartsView extends ActivatableViewAndModel tabPaneSelectionModel; private TableColumn priceColumn, volumeColumn, marketColumn; - private final ObservableList listItems = FXCollections.observableArrayList(); - private final SortedList sortedList = new SortedList<>(listItems); + private SortedList sortedList = new SortedList<>(FXCollections.observableArrayList()); private ChangeListener timeUnitChangeListener; private ChangeListener priceAxisYWidthListener; @@ -241,7 +242,7 @@ public class TradesChartsView extends ActivatableViewAndModel { - nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.selectedTradeStatistics.size())); + nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.tradeStatisticsByCurrency.size())); fillList(); }; parentHeightListener = (observable, oldValue, newValue) -> layout(); @@ -281,12 +282,10 @@ public class TradesChartsView extends ActivatableViewAndModel model.setSelectedTabIndex((int) newValue); model.setSelectedTabIndex(tabPaneSelectionModel.getSelectedIndex()); @@ -319,27 +318,21 @@ public class TradesChartsView extends ActivatableViewAndModel { }); - sortedList.comparatorProperty().bind(tableView.comparatorProperty()); - boolean useAnimations = model.preferences.isUseAnimations(); priceChart.setAnimated(useAnimations); volumeChart.setAnimated(useAnimations); volumeInUsdChart.setAnimated(useAnimations); - priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); - volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); - volumeInUsdAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); - nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.selectedTradeStatistics.size())); + nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.tradeStatisticsByCurrency.size())); exportLink.setOnAction(e -> exportToCsv()); - UserThread.runAfter(this::updateChartData, 100, TimeUnit.MILLISECONDS); if (root.getParent() instanceof Pane) { rootParent = (Pane) root.getParent(); @@ -357,8 +350,6 @@ public class TradesChartsView extends ActivatableViewAndModel tradeStatistics3ListItems = model.selectedTradeStatistics.stream() - .map(tradeStatistics -> new TradeStatistics3ListItem(tradeStatistics, - coinFormatter, - model.showAllTradeCurrenciesProperty.get())) - .collect(Collectors.toList()); - listItems.setAll(tradeStatistics3ListItems); + long ts = System.currentTimeMillis(); + CompletableFuture.supplyAsync(() -> { + return model.tradeStatisticsByCurrency.stream() + .map(tradeStatistics -> new TradeStatistics3ListItem(tradeStatistics, + coinFormatter, + model.showAllTradeCurrenciesProperty.get())) + .collect(Collectors.toCollection(FXCollections::observableArrayList)); + }).whenComplete((listItems, throwable) -> { + log.debug("Creating listItems took {} ms", System.currentTimeMillis() - ts); + + long ts2 = System.currentTimeMillis(); + sortedList.comparatorProperty().unbind(); + // Sorting is slow as we have > 100k items. So we prefer to do it on the non UI thread. + sortedList = new SortedList<>(listItems); + sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + log.debug("Created sorted list took {} ms", System.currentTimeMillis() - ts2); + UserThread.execute(() -> { + // When we attach the list to the table we need to be on the UI thread. + tableView.setItems(sortedList); + }); + }); } private void exportToCsv() { @@ -459,12 +465,11 @@ public class TradesChartsView extends ActivatableViewAndModel(); - priceAxisX = new NumberAxis(0, model.maxTicks + 1, 1); + priceAxisX = new NumberAxis(0, MAX_TICKS + 1, 1); priceAxisX.setTickUnit(4); priceAxisX.setMinorTickCount(4); priceAxisX.setMinorTickVisible(true); priceAxisX.setForceZeroInRange(false); - priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); addTickMarkLabelCssClass(priceAxisX, "axis-tick-mark-text-node"); priceAxisY = new NumberAxis(); @@ -525,11 +530,11 @@ public class TradesChartsView extends ActivatableViewAndModel selectedTradeStatistics = FXCollections.observableArrayList(); + final ObservableList tradeStatisticsByCurrency = FXCollections.observableArrayList(); final ObservableList> priceItems = FXCollections.observableArrayList(); final ObservableList> volumeItems = FXCollections.observableArrayList(); final ObservableList> volumeInUsdItems = FXCollections.observableArrayList(); - private Map>> itemsPerInterval; + private final Map>> itemsPerInterval = new HashMap<>(); TickUnit tickUnit; - final int maxTicks = 90; private int selectedTabIndex; - final Map> usdPriceMapsPerTickUnit = new HashMap<>(); + final Map> usdAveragePriceMapsPerTickUnit = new HashMap<>(); private boolean fillTradeCurrenciesOnActivateCalled; + private volatile boolean deactivateCalled; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -132,8 +118,17 @@ class TradesChartsViewModel extends ActivatableViewModel { this.navigation = navigation; setChangeListener = change -> { - updateSelectedTradeStatistics(getCurrencyCode()); - updateChartData(); + applyAsyncTradeStatisticsForCurrency(getCurrencyCode()) + .whenComplete((result, throwable) -> { + if (deactivateCalled) { + return; + } + if (throwable != null) { + log.error("Error at setChangeListener/applyAsyncTradeStatisticsForCurrency. {}", throwable.toString()); + return; + } + applyAsyncChartData(); + }); fillTradeCurrencies(); }; @@ -151,23 +146,149 @@ class TradesChartsViewModel extends ActivatableViewModel { @Override protected void activate() { + long ts = System.currentTimeMillis(); + deactivateCalled = false; + tradeStatisticsManager.getObservableTradeStatisticsSet().addListener(setChangeListener); if (!fillTradeCurrenciesOnActivateCalled) { fillTradeCurrencies(); fillTradeCurrenciesOnActivateCalled = true; } - buildUsdPricesPerDay(); - updateSelectedTradeStatistics(getCurrencyCode()); - updateChartData(); syncPriceFeedCurrency(); setMarketPriceFeedCurrency(); + + List> allFutures = new ArrayList<>(); + CompletableFuture task1Done = new CompletableFuture<>(); + allFutures.add(task1Done); + CompletableFuture task2Done = new CompletableFuture<>(); + allFutures.add(task2Done); + CompletableFutureUtils.allOf(allFutures) + .whenComplete((res, throwable) -> { + if (deactivateCalled) { + return; + } + if (throwable != null) { + log.error(throwable.toString()); + return; + } + //Once applyAsyncUsdAveragePriceMapsPerTickUnit and applyAsyncTradeStatisticsForCurrency are + // both completed we call applyAsyncChartData + UserThread.execute(this::applyAsyncChartData); + }); + + // We call applyAsyncUsdAveragePriceMapsPerTickUnit and applyAsyncTradeStatisticsForCurrency + // in parallel for better performance + applyAsyncUsdAveragePriceMapsPerTickUnit(task1Done); + applyAsyncTradeStatisticsForCurrency(getCurrencyCode(), task2Done); + + log.debug("activate took {}", System.currentTimeMillis() - ts); } @Override protected void deactivate() { + deactivateCalled = true; tradeStatisticsManager.getObservableTradeStatisticsSet().removeListener(setChangeListener); + + // We want to avoid to trigger listeners in the view so we delay a bit. Deactivate on model is called before + // deactivate on view. + UserThread.execute(() -> { + usdAveragePriceMapsPerTickUnit.clear(); + tradeStatisticsByCurrency.clear(); + priceItems.clear(); + volumeItems.clear(); + volumeInUsdItems.clear(); + itemsPerInterval.clear(); + }); } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Async calls + /////////////////////////////////////////////////////////////////////////////////////////// + + private void applyAsyncUsdAveragePriceMapsPerTickUnit(CompletableFuture completeFuture) { + long ts = System.currentTimeMillis(); + ChartCalculations.getUsdAveragePriceMapsPerTickUnit(tradeStatisticsManager.getObservableTradeStatisticsSet()) + .whenComplete((usdAveragePriceMapsPerTickUnit, throwable) -> { + if (deactivateCalled) { + return; + } + if (throwable != null) { + log.error("Error at applyAsyncUsdAveragePriceMapsPerTickUnit. {}", throwable.toString()); + completeFuture.completeExceptionally(throwable); + return; + } + UserThread.execute(() -> { + this.usdAveragePriceMapsPerTickUnit.clear(); + this.usdAveragePriceMapsPerTickUnit.putAll(usdAveragePriceMapsPerTickUnit); + log.debug("applyAsyncUsdAveragePriceMapsPerTickUnit took {}", System.currentTimeMillis() - ts); + completeFuture.complete(true); + }); + }); + } + + private CompletableFuture applyAsyncTradeStatisticsForCurrency(String currencyCode) { + return applyAsyncTradeStatisticsForCurrency(currencyCode, null); + } + + private CompletableFuture applyAsyncTradeStatisticsForCurrency(String currencyCode, + @Nullable CompletableFuture completeFuture) { + CompletableFuture future = new CompletableFuture<>(); + long ts = System.currentTimeMillis(); + ChartCalculations.getTradeStatisticsForCurrency(tradeStatisticsManager.getObservableTradeStatisticsSet(), + currencyCode, + showAllTradeCurrenciesProperty.get()) + .whenComplete((list, throwable) -> { + if (deactivateCalled) { + return; + } + if (throwable != null) { + log.error("Error at applyAsyncTradeStatisticsForCurrency. {}", throwable.toString()); + if (completeFuture != null) { + completeFuture.completeExceptionally(throwable); + } + return; + } + + UserThread.execute(() -> { + tradeStatisticsByCurrency.setAll(list); + log.debug("applyAsyncTradeStatisticsForCurrency took {}", System.currentTimeMillis() - ts); + if (completeFuture != null) { + completeFuture.complete(true); + } + future.complete(true); + }); + }); + return future; + } + + private void applyAsyncChartData() { + long ts = System.currentTimeMillis(); + ChartCalculations.getUpdateChartResult(new ArrayList<>(tradeStatisticsByCurrency), + tickUnit, + usdAveragePriceMapsPerTickUnit, + getCurrencyCode()) + .whenComplete((updateChartResult, throwable) -> { + if (deactivateCalled) { + return; + } + if (throwable != null) { + log.error("Error at applyAsyncChartData. {}", throwable.toString()); + return; + } + UserThread.execute(() -> { + itemsPerInterval.clear(); + itemsPerInterval.putAll(updateChartResult.getItemsPerInterval()); + + priceItems.setAll(updateChartResult.getPriceItems()); + volumeItems.setAll(updateChartResult.getVolumeItems()); + volumeInUsdItems.setAll(updateChartResult.getVolumeInUsdItems()); + log.debug("applyAsyncChartData took {}", System.currentTimeMillis() - ts); + }); + }); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// @@ -192,15 +313,24 @@ class TradesChartsViewModel extends ActivatableViewModel { } preferences.setTradeChartsScreenCurrencyCode(code); - updateSelectedTradeStatistics(getCurrencyCode()); - updateChartData(); + applyAsyncTradeStatisticsForCurrency(getCurrencyCode()) + .whenComplete((result, throwable) -> { + if (deactivateCalled) { + return; + } + if (throwable != null) { + log.error("Error at onSetTradeCurrency/applyAsyncTradeStatisticsForCurrency. {}", throwable.toString()); + return; + } + applyAsyncChartData(); + }); } } void setTickUnit(TickUnit tickUnit) { this.tickUnit = tickUnit; preferences.setTradeStatisticsTickUnitIndex(tickUnit.ordinal()); - updateChartData(); + applyAsyncChartData(); } void setSelectedTabIndex(int selectedTabIndex) { @@ -209,6 +339,7 @@ class TradesChartsViewModel extends ActivatableViewModel { setMarketPriceFeedCurrency(); } + /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// @@ -225,6 +356,11 @@ class TradesChartsViewModel extends ActivatableViewModel { return currencyListItems.getObservableList().stream().filter(e -> e.tradeCurrency.equals(selectedTradeCurrencyProperty.get())).findAny(); } + long getTimeFromTickIndex(long tick) { + return ChartCalculations.getTimeFromTickIndex(tick, itemsPerInterval); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// @@ -234,7 +370,6 @@ class TradesChartsViewModel extends ActivatableViewModel { List tradeCurrencyList = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .flatMap(e -> CurrencyUtil.getTradeCurrency(e.getCurrency()).stream()) .collect(Collectors.toList()); - currencyListItems.updateWithCurrencies(tradeCurrencyList, showAllCurrencyListItem); } @@ -252,197 +387,6 @@ class TradesChartsViewModel extends ActivatableViewModel { priceFeedService.setCurrencyCode(selectedTradeCurrencyProperty.get().getCode()); } - private void buildUsdPricesPerDay() { - if (usdPriceMapsPerTickUnit.isEmpty()) { - Map>> dateMapsPerTickUnit = new HashMap<>(); - for (TickUnit tick : TickUnit.values()) { - dateMapsPerTickUnit.put(tick, new HashMap<>()); - } - - tradeStatisticsManager.getObservableTradeStatisticsSet().stream() - .filter(e -> e.getCurrency().equals("USD")) - .forEach(tradeStatistics -> { - for (TickUnit tick : TickUnit.values()) { - long time = roundToTick(tradeStatistics.getLocalDateTime(), tick).getTime(); - Map> map = dateMapsPerTickUnit.get(tick); - map.putIfAbsent(time, new ArrayList<>()); - map.get(time).add(tradeStatistics); - } - }); - - dateMapsPerTickUnit.forEach((tick, map) -> { - HashMap priceMap = new HashMap<>(); - map.forEach((date, tradeStatisticsList) -> priceMap.put(date, getAveragePrice(tradeStatisticsList))); - usdPriceMapsPerTickUnit.put(tick, priceMap); - }); - } - } - - private long getAveragePrice(List tradeStatisticsList) { - long accumulatedAmount = 0; - long accumulatedVolume = 0; - for (TradeStatistics3 tradeStatistics : tradeStatisticsList) { - accumulatedAmount += tradeStatistics.getAmount(); - accumulatedVolume += tradeStatistics.getTradeVolume().getValue(); - } - - double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT); - return MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount); - } - - private void updateChartData() { - // Generate date range and create sets for all ticks - itemsPerInterval = new HashMap<>(); - Date time = new Date(); - for (long i = maxTicks + 1; i >= 0; --i) { - Pair> pair = new Pair<>((Date) time.clone(), new HashSet<>()); - itemsPerInterval.put(i, pair); - // We adjust the time for the next iteration - time.setTime(time.getTime() - 1); - time = roundToTick(time, tickUnit); - } - - // Get all entries for the defined time interval - selectedTradeStatistics.forEach(tradeStatistics -> { - for (long i = maxTicks; i > 0; --i) { - Pair> pair = itemsPerInterval.get(i); - if (tradeStatistics.getDate().after(pair.getKey())) { - pair.getValue().add(tradeStatistics); - break; - } - } - }); - - Map map = usdPriceMapsPerTickUnit.get(tickUnit); - AtomicLong averageUsdPrice = new AtomicLong(0); - - // create CandleData for defined time interval - List candleDataList = itemsPerInterval.entrySet().stream() - .filter(entry -> entry.getKey() >= 0 && !entry.getValue().getValue().isEmpty()) - .map(entry -> { - long tickStartDate = entry.getValue().getKey().getTime(); - // If we don't have a price we take the previous one - if (map.containsKey(tickStartDate)) { - averageUsdPrice.set(map.get(tickStartDate)); - } - return getCandleData(entry.getKey(), entry.getValue().getValue(), averageUsdPrice.get()); - }) - .sorted(Comparator.comparingLong(o -> o.tick)) - .collect(Collectors.toList()); - - priceItems.setAll(candleDataList.stream() - .map(e -> new XYChart.Data(e.tick, e.open, e)) - .collect(Collectors.toList())); - - volumeItems.setAll(candleDataList.stream() - .map(candleData -> new XYChart.Data(candleData.tick, candleData.accumulatedAmount, candleData)) - .collect(Collectors.toList())); - - volumeInUsdItems.setAll(candleDataList.stream() - .map(candleData -> new XYChart.Data(candleData.tick, candleData.volumeInUsd, candleData)) - .collect(Collectors.toList())); - } - - private void updateSelectedTradeStatistics(String currencyCode) { - selectedTradeStatistics.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() - .filter(e -> showAllTradeCurrenciesProperty.get() || e.getCurrency().equals(currencyCode)) - .collect(Collectors.toList())); - } - - @VisibleForTesting - CandleData getCandleData(long tick, Set set, long averageUsdPrice) { - long open = 0; - long close = 0; - long high = 0; - long low = 0; - long accumulatedVolume = 0; - long accumulatedAmount = 0; - long numTrades = set.size(); - List tradePrices = new ArrayList<>(); - for (TradeStatistics3 item : set) { - long tradePriceAsLong = item.getTradePrice().getValue(); - // Previously a check was done which inverted the low and high for cryptocurrencies. - low = (low != 0) ? Math.min(low, tradePriceAsLong) : tradePriceAsLong; - high = (high != 0) ? Math.max(high, tradePriceAsLong) : tradePriceAsLong; - - accumulatedVolume += item.getTradeVolume().getValue(); - accumulatedAmount += item.getTradeAmount().getValue(); - tradePrices.add(tradePriceAsLong); - } - Collections.sort(tradePrices); - - List list = new ArrayList<>(set); - list.sort(Comparator.comparingLong(TradeStatistics3::getDateAsLong)); - if (list.size() > 0) { - open = list.get(0).getTradePrice().getValue(); - close = list.get(list.size() - 1).getTradePrice().getValue(); - } - - long averagePrice; - Long[] prices = new Long[tradePrices.size()]; - tradePrices.toArray(prices); - long medianPrice = MathUtils.getMedian(prices); - boolean isBullish; - if (CurrencyUtil.isCryptoCurrency(getCurrencyCode())) { - isBullish = close < open; - double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, Altcoin.SMALLEST_UNIT_EXPONENT); - averagePrice = MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / (double) accumulatedVolume); - } else { - isBullish = close > open; - double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT); - averagePrice = MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount); - } - - Date dateFrom = new Date(getTimeFromTickIndex(tick)); - Date dateTo = new Date(getTimeFromTickIndex(tick + 1)); - String dateString = tickUnit.ordinal() > TickUnit.DAY.ordinal() ? - DisplayUtils.formatDateTimeSpan(dateFrom, dateTo) : - DisplayUtils.formatDate(dateFrom) + " - " + DisplayUtils.formatDate(dateTo); - - // We do not need precision, so we scale down before multiplication otherwise we could get an overflow. - averageUsdPrice = (long) MathUtils.scaleDownByPowerOf10((double) averageUsdPrice, 4); - long volumeInUsd = averageUsdPrice * (long) MathUtils.scaleDownByPowerOf10((double) accumulatedAmount, 4); - // We store USD value without decimals as its only total volume, no precision is needed. - volumeInUsd = (long) MathUtils.scaleDownByPowerOf10((double) volumeInUsd, 4); - return new CandleData(tick, open, close, high, low, averagePrice, medianPrice, accumulatedAmount, accumulatedVolume, - numTrades, isBullish, dateString, volumeInUsd); - } - - Date roundToTick(Date time, TickUnit tickUnit) { - return roundToTick(time.toInstant().atZone(ZONE_ID).toLocalDateTime(), tickUnit); - } - - Date roundToTick(LocalDateTime localDate, TickUnit tickUnit) { - switch (tickUnit) { - case YEAR: - return Date.from(localDate.withMonth(1).withDayOfYear(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); - case MONTH: - return Date.from(localDate.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); - case WEEK: - int dayOfWeek = localDate.getDayOfWeek().getValue(); - LocalDateTime firstDayOfWeek = ChronoUnit.DAYS.addTo(localDate, 1 - dayOfWeek); - return Date.from(firstDayOfWeek.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); - case DAY: - return Date.from(localDate.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); - case HOUR: - return Date.from(localDate.withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); - case MINUTE_10: - return Date.from(localDate.withMinute(localDate.getMinute() - localDate.getMinute() % 10).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); - default: - return Date.from(localDate.atZone(ZONE_ID).toInstant()); - } - } - - private long getTimeFromTick(long tick) { - if (itemsPerInterval == null || itemsPerInterval.get(tick) == null) return 0; - return itemsPerInterval.get(tick).getKey().getTime(); - } - - long getTimeFromTickIndex(long index) { - if (index > maxTicks + 1) return 0; - return getTimeFromTick(index); - } - private boolean isShowAllEntry(@Nullable String id) { return id != null && id.equals(GUIUtil.SHOW_ALL_FLAG); } diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java index 080f39f57e..774ab81bd9 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java @@ -18,9 +18,9 @@ package bisq.desktop.main.market.trades.charts.volume; import bisq.desktop.main.market.trades.charts.CandleData; -import bisq.desktop.util.DisplayUtils; import bisq.core.locale.Res; +import bisq.core.util.VolumeUtil; import javafx.scene.Group; import javafx.scene.control.Tooltip; @@ -57,7 +57,7 @@ public class VolumeBar extends Group { public void update(double height, double candleWidth, CandleData candleData) { bar.resizeRelocate(-candleWidth / 2, 0, candleWidth, height); String volumeInBtc = volumeStringConverter.toString(candleData.accumulatedAmount); - String volumeInUsd = DisplayUtils.formatLargeFiat(candleData.volumeInUsd, "USD"); + String volumeInUsd = VolumeUtil.formatLargeFiat(candleData.volumeInUsd, "USD"); tooltip.setText(Res.get("market.trades.tooltip.volumeBar", volumeInBtc, volumeInUsd, candleData.numTrades, candleData.date)); } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/BuyOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/BuyOfferView.java index 56dbe1cd3c..c525425c9d 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/BuyOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/BuyOfferView.java @@ -20,7 +20,7 @@ package bisq.desktop.main.offer; import bisq.desktop.Navigation; import bisq.desktop.common.view.FxmlView; import bisq.desktop.common.view.ViewLoader; - +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferPayload; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.user.Preferences; @@ -37,15 +37,13 @@ public class BuyOfferView extends OfferView { public BuyOfferView(ViewLoader viewLoader, Navigation navigation, Preferences preferences, - ArbitratorManager arbitratorManager, User user, P2PService p2PService) { super(viewLoader, navigation, preferences, - arbitratorManager, user, p2PService, - OfferPayload.Direction.BUY); + OfferDirection.BUY); } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/ClosableView.java b/desktop/src/main/java/bisq/desktop/main/offer/ClosableView.java new file mode 100644 index 0000000000..1cd27e0fe2 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/offer/ClosableView.java @@ -0,0 +1,22 @@ +/* + * 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 . + */ + +package bisq.desktop.main.offer; + +public interface ClosableView { + public void setCloseHandler(OfferView.CloseHandler closeHandler); +} diff --git a/desktop/src/main/java/bisq/desktop/main/offer/InitializableViewWithTakeOfferData.java b/desktop/src/main/java/bisq/desktop/main/offer/InitializableViewWithTakeOfferData.java new file mode 100644 index 0000000000..2d03add918 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/offer/InitializableViewWithTakeOfferData.java @@ -0,0 +1,26 @@ +/* + * 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 . + */ + +package bisq.desktop.main.offer; + +import bisq.core.offer.Offer; + +public interface InitializableViewWithTakeOfferData { + + public void initWithData(Offer offer); + +} diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java index 0ede059f8e..6f57f1f780 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java @@ -33,7 +33,7 @@ import bisq.core.monetary.Price; import bisq.core.monetary.Volume; import bisq.core.offer.CreateOfferService; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOfferManager; import bisq.core.payment.PaymentAccount; @@ -56,7 +56,7 @@ import bisq.common.util.Tuple2; import bisq.common.util.Utilities; import org.bitcoinj.core.Coin; - +import org.jetbrains.annotations.NotNull; import com.google.inject.Inject; import javax.inject.Named; @@ -83,7 +83,9 @@ import java.math.BigInteger; import java.util.Comparator; import java.util.Date; import java.util.HashSet; +import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -91,6 +93,7 @@ import lombok.Getter; import javax.annotation.Nullable; +import static bisq.core.payment.payload.PaymentMethod.HAL_CASH_ID; import static com.google.common.base.Preconditions.checkNotNull; import static java.util.Comparator.comparing; @@ -111,7 +114,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { private final XmrBalanceListener xmrBalanceListener; private final SetChangeListener paymentAccountsChangeListener; - protected OfferPayload.Direction direction; + protected OfferDirection direction; protected TradeCurrency tradeCurrency; protected final StringProperty tradeCurrencyCode = new SimpleStringProperty(); protected final BooleanProperty useMarketBasedPrice = new SimpleBooleanProperty(); @@ -176,7 +179,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { this.navigation = navigation; this.tradeStatisticsManager = tradeStatisticsManager; - offerId = createOfferService.getRandomOfferId(); + offerId = OfferUtil.getRandomOfferId(); shortOfferId = Utilities.getShortId(offerId); addressEntry = xmrWalletService.getOrCreateAddressEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING); @@ -224,7 +227,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { /////////////////////////////////////////////////////////////////////////////////////////// // called before activate() - public boolean initWithData(OfferPayload.Direction direction, TradeCurrency tradeCurrency) { + public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) { this.direction = direction; this.tradeCurrency = tradeCurrency; @@ -245,7 +248,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { if (account != null) { this.paymentAccount = account; } else { - Optional paymentAccountOptional = paymentAccounts.stream().findAny(); + Optional paymentAccountOptional = getAnyPaymentAccount(); if (paymentAccountOptional.isPresent()) { this.paymentAccount = paymentAccountOptional.get(); @@ -275,6 +278,17 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return true; } + @NotNull + private Optional getAnyPaymentAccount() { + if (CurrencyUtil.isFiatCurrency(tradeCurrency.getCode())) { + return paymentAccounts.stream().filter(paymentAccount1 -> !paymentAccount1.getPaymentMethod().isAltcoin()).findAny(); + } else { + return paymentAccounts.stream().filter(paymentAccount1 -> paymentAccount1.getPaymentMethod().isAltcoin() && + paymentAccount1.getTradeCurrency().isPresent() && + !Objects.equals(paymentAccount1.getTradeCurrency().get().getCode(), GUIUtil.TOP_ALTCOIN.getCode())).findAny(); + } + } + protected PaymentAccount getPreselectedPaymentAccount() { return preferences.getSelectedPaymentAccountForCreateOffer(); } @@ -323,10 +337,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel { void onPaymentAccountSelected(PaymentAccount paymentAccount) { if (paymentAccount != null && !this.paymentAccount.equals(paymentAccount)) { - volume.set(null); - minVolume.set(null); - price.set(null); - marketPriceMargin = 0; preferences.setSelectedPaymentAccountForCreateOffer(paymentAccount); this.paymentAccount = paymentAccount; @@ -346,8 +356,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return; } // Get average historic prices over for the prior trade period equaling the lock time - var blocksRange = Restrictions.getLockTime(paymentAccount.getPaymentMethod().isAsset()); - var startDate = new Date(System.currentTimeMillis() - blocksRange * 10 * 60000); + var blocksRange = Restrictions.getLockTime(paymentAccount.getPaymentMethod().isBlockchain()); + var startDate = new Date(System.currentTimeMillis() - blocksRange * 10L * 60000); var sortedRangeData = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .filter(e -> e.getCurrency().equals(getTradeCurrency().getCode())) .filter(e -> e.getDate().compareTo(startDate) >= 0) @@ -406,7 +416,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { Optional tradeCurrencyOptional = preferences.getTradeCurrenciesAsObservable() .stream().filter(e -> e.getCode().equals(code)).findAny(); - if (!tradeCurrencyOptional.isPresent()) { + if (tradeCurrencyOptional.isEmpty()) { if (CurrencyUtil.isCryptoCurrency(code)) { CurrencyUtil.getCryptoCurrency(code).ifPresent(preferences::addCryptoCurrency); } else { @@ -425,7 +435,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } } - protected void setMarketPriceMargin(double marketPriceMargin) { + protected void setMarketPriceMarginPct(double marketPriceMargin) { this.marketPriceMargin = marketPriceMargin; } @@ -449,16 +459,16 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return true; } - OfferPayload.Direction getDirection() { + public OfferDirection getDirection() { return direction; } boolean isSellOffer() { - return direction == OfferPayload.Direction.SELL; + return direction == OfferDirection.SELL; } boolean isBuyOffer() { - return direction == OfferPayload.Direction.BUY; + return direction == OfferDirection.BUY; } XmrAddressEntry getAddressEntry() { @@ -482,7 +492,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return paymentAccounts; } - public double getMarketPriceMargin() { + public double getMarketPriceMarginPct() { return marketPriceMargin; } @@ -500,9 +510,9 @@ public abstract class MutableOfferDataModel extends OfferDataModel { double calculateMarketPriceManual(double marketPrice, double volumeAsDouble, double amountAsDouble) { double manualPriceAsDouble = offerUtil.calculateManualPrice(volumeAsDouble, amountAsDouble); - double percentage = offerUtil.calculateMarketPriceMargin(manualPriceAsDouble, marketPrice); + double percentage = offerUtil.calculateMarketPriceMarginPct(manualPriceAsDouble, marketPrice); - setMarketPriceMargin(percentage); + setMarketPriceMarginPct(percentage); return manualPriceAsDouble; } @@ -540,7 +550,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { Volume volumeByAmount = price.get().getVolumeByAmount(minAmount.get()); // For HalCash we want multiple of 10 EUR - if (paymentAccount.isHalCashAccount()) + if (isUsingHalCashAccount()) volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); @@ -551,7 +561,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { if (isNonZeroPrice.test(price) && isNonZeroVolume.test(volume) && allowAmountUpdate) { try { Coin value = DisplayUtils.reduceTo4Decimals(price.get().getAmountByVolume(volume.get()), btcFormatter); - if (paymentAccount.isHalCashAccount()) + if (isUsingHalCashAccount()) value = CoinUtil.getAdjustedAmountForHalCash(value, price.get(), getMaxTradeLimit()); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) value = CoinUtil.getRoundedFiatAmount(value, price.get(), getMaxTradeLimit()); @@ -598,6 +608,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel { paymentAccounts.sort(comparing(PaymentAccount::getAccountName)); } + protected abstract Set getUserPaymentAccounts(); + protected void setAmount(Coin amount) { this.amount.set(amount); } @@ -610,14 +622,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel { this.volume.set(volume); } - void setBuyerSecurityDeposit(double value) { + protected void setBuyerSecurityDeposit(double value) { this.buyerSecurityDeposit.set(value); } - protected boolean isUseMarketBasedPriceValue() { - return marketPriceAvailable && useMarketBasedPrice.get() && !paymentAccount.isHalCashAccount(); - } - /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// @@ -685,7 +693,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return getBoundedSellerSecurityDepositAsCoin(percentOfAmountAsCoin); } - private Coin getBoundedBuyerSecurityDepositAsCoin(Coin value) { + protected Coin getBoundedBuyerSecurityDepositAsCoin(Coin value) { // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the // MinBuyerSecurityDepositAsCoin from Restrictions. return Coin.valueOf(Math.max(Restrictions.getMinBuyerSecurityDepositAsCoin().value, value.value)); @@ -725,4 +733,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel { public void setTriggerPrice(long triggerPrice) { this.triggerPrice = triggerPrice; } + + public boolean isUsingHalCashAccount() { + return paymentAccount.hasPaymentMethodWithId(HAL_CASH_ID); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java index d4538b3db1..b2164bf087 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java @@ -22,6 +22,7 @@ import bisq.desktop.common.view.ActivatableViewAndModel; import bisq.desktop.components.AddressTextField; import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.AutoTooltipSlideToggleButton; import bisq.desktop.components.BalanceTextField; import bisq.desktop.components.BusyAnimation; import bisq.desktop.components.FundsTextField; @@ -31,6 +32,10 @@ import bisq.desktop.components.TitledGroupBg; import bisq.desktop.main.MainView; import bisq.desktop.main.account.AccountView; import bisq.desktop.main.account.content.fiataccounts.FiatAccountsView; +import bisq.desktop.main.offer.ClosableView; +import bisq.desktop.main.offer.OfferView; +import bisq.desktop.main.offer.OfferViewUtil; +import bisq.desktop.main.offer.SelectableView; import bisq.desktop.main.overlays.notifications.Notification; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.OfferDetailsWindow; @@ -44,7 +49,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferDirection; import bisq.core.payment.FasterPaymentsAccount; import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentMethod; @@ -77,7 +82,6 @@ import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; @@ -99,6 +103,7 @@ import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.util.StringConverter; @@ -107,15 +112,20 @@ import java.net.URI; import java.io.ByteArrayInputStream; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.concurrent.TimeUnit; +import lombok.Setter; + import org.jetbrains.annotations.NotNull; +import static bisq.core.payment.payload.PaymentMethod.HAL_CASH_ID; +import static bisq.desktop.main.offer.OfferViewUtil.addPayInfoEntry; import static bisq.desktop.util.FormBuilder.*; import static javafx.beans.binding.Bindings.createStringBinding; -public abstract class MutableOfferView> extends ActivatableViewAndModel { +public abstract class MutableOfferView> extends ActivatableViewAndModel implements ClosableView, SelectableView { protected final Navigation navigation; private final Preferences preferences; private final OfferDetailsWindow offerDetailsWindow; @@ -126,7 +136,7 @@ public abstract class MutableOfferView> exten private TitledGroupBg payFundsTitledGroupBg, setDepositTitledGroupBg, paymentTitledGroupBg; protected TitledGroupBg amountTitledGroupBg; private BusyAnimation waitingForFundsSpinner; - private AutoTooltipButton nextButton, cancelButton1, cancelButton2, placeOfferButton; + private AutoTooltipButton nextButton, cancelButton1, cancelButton2, placeOfferButton, fundFromSavingsWalletButton; private Button priceTypeToggleButton; private InputTextField fixedPriceTextField, marketBasedPriceTextField, triggerPriceInputTextField; protected InputTextField amountTextField, minAmountTextField, volumeTextField, buyerSecurityDepositInputTextField; @@ -161,11 +171,15 @@ public abstract class MutableOfferView> exten protected int gridRow = 0; private final List editOfferElements = new ArrayList<>(); + private final HashMap paymentAccountWarningDisplayed = new HashMap<>(); private boolean clearXchangeWarningDisplayed, fasterPaymentsWarningDisplayed, isActivated; private InfoInputTextField marketBasedPriceInfoInputTextField, volumeInfoInputTextField, buyerSecurityDepositInfoInputTextField, triggerPriceInfoInputTextField; private Text xIcon, fakeXIcon; + @Setter + private OfferView.OfferActionHandler offerActionHandler; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -198,7 +212,7 @@ public abstract class MutableOfferView> exten balanceTextField.setFormatter(model.getBtcFormatter()); paymentAccountsComboBox.setConverter(GUIUtil.getPaymentAccountsComboBoxStringConverter()); - paymentAccountsComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("shared.selectTradingAccount"), + paymentAccountsComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("shared.chooseTradingAccount"), paymentAccountsComboBox, false)); paymentAccountsComboBox.setCellFactory(model.getPaymentAccountListCellFactory(paymentAccountsComboBox)); @@ -233,9 +247,9 @@ public abstract class MutableOfferView> exten addressTextField.setAddress(model.getAddressAsString()); addressTextField.setPaymentLabel(model.getPaymentLabel()); - paymentAccountsComboBox.setItems(model.getDataModel().getPaymentAccounts()); - paymentAccountsComboBox.getSelectionModel().select(model.getPaymentAccount()); currencyComboBox.getSelectionModel().select(model.getTradeCurrency()); + paymentAccountsComboBox.setItems(getPaymentAccounts()); + paymentAccountsComboBox.getSelectionModel().select(model.getPaymentAccount()); onPaymentAccountsComboBoxSelected(); @@ -265,17 +279,22 @@ public abstract class MutableOfferView> exten // API /////////////////////////////////////////////////////////////////////////////////////////// + @Override public void onTabSelected(boolean isSelected) { - if (isSelected && !model.getDataModel().isTabSelected) + if (isSelected && !model.getDataModel().isTabSelected) { doActivate(); - else + } else { deactivate(); + } isActivated = isSelected; model.getDataModel().onTabSelected(isSelected); } - public void initWithData(OfferPayload.Direction direction, TradeCurrency tradeCurrency) { + public void initWithData(OfferDirection direction, TradeCurrency tradeCurrency, + OfferView.OfferActionHandler offerActionHandler) { + this.offerActionHandler = offerActionHandler; + boolean result = model.initWithData(direction, tradeCurrency); if (!result) { @@ -288,19 +307,33 @@ public abstract class MutableOfferView> exten }).show(); } - if (direction == OfferPayload.Direction.BUY) { + String placeOfferButtonLabel; + + if (OfferViewUtil.isShownAsBuyOffer(direction, tradeCurrency)) { placeOfferButton.setId("buy-button-big"); - placeOfferButton.updateText(Res.get("createOffer.placeOfferButton", Res.get("shared.buy"))); + if (CurrencyUtil.isFiatCurrency(tradeCurrency.getCode())) { + placeOfferButtonLabel = Res.get("createOffer.placeOfferButton", Res.get("shared.buy")); + } else { + placeOfferButtonLabel = Res.get("createOffer.placeOfferButtonAltcoin", Res.get("shared.buy"), tradeCurrency.getCode()); + } + nextButton.setId("buy-button"); + fundFromSavingsWalletButton.setId("buy-button"); } else { placeOfferButton.setId("sell-button-big"); - placeOfferButton.updateText(Res.get("createOffer.placeOfferButton", Res.get("shared.sell"))); + if (CurrencyUtil.isFiatCurrency(tradeCurrency.getCode())) { + placeOfferButtonLabel = Res.get("createOffer.placeOfferButton", Res.get("shared.sell")); + } else { + placeOfferButtonLabel = Res.get("createOffer.placeOfferButtonAltcoin", Res.get("shared.sell"), tradeCurrency.getCode()); + } + nextButton.setId("sell-button"); + fundFromSavingsWalletButton.setId("sell-button"); } + placeOfferButton.updateText(placeOfferButtonLabel); updatePriceToggle(); - } - // called form parent as the view does not get notified when the tab is closed + // called from parent as the view does not get notified when the tab is closed public void onClose() { // we use model.placeOfferCompleted to not react on close which was triggered by a successful placeOffer if (model.getDataModel().getBalance().get().isPositive() && !model.placeOfferCompleted.get()) { @@ -308,6 +341,7 @@ public abstract class MutableOfferView> exten } } + @Override public void setCloseHandler(OfferView.CloseHandler closeHandler) { this.closeHandler = closeHandler; } @@ -389,7 +423,7 @@ public abstract class MutableOfferView> exten String key = "securityDepositInfo"; new Popup().backgroundInfo(Res.get("popup.info.securityDepositInfo")) .actionButtonText(Res.get("shared.faq")) - .onAction(() -> GUIUtil.openWebPage("https://bisq.network/faq#6")) + .onAction(() -> GUIUtil.openWebPage("https://bisq.wiki/Frequently_asked_questions#Why_does_Bisq_require_a_security_deposit_in_BTC.3F")) .useIUnderstandButton() .dontShowAgainId(key) .show(); @@ -452,6 +486,11 @@ public abstract class MutableOfferView> exten } } + private void maybeShowAccountWarning(PaymentAccount paymentAccount, boolean isBuyer) { + String msgKey = paymentAccount.getPreTradeMessage(isBuyer); + OfferViewUtil.showPaymentAccountWarning(msgKey, paymentAccountWarningDisplayed); + } + protected void onPaymentAccountsComboBoxSelected() { // Temporary deactivate handler as the payment account change can populate a new currency list and causes // unwanted selection events (item 0) @@ -461,20 +500,23 @@ public abstract class MutableOfferView> exten if (paymentAccount != null) { maybeShowClearXchangeWarning(paymentAccount); maybeShowFasterPaymentsWarning(paymentAccount); + maybeShowAccountWarning(paymentAccount, model.getDataModel().isBuyOffer()); currencySelection.setVisible(paymentAccount.hasMultipleCurrencies()); currencySelection.setManaged(paymentAccount.hasMultipleCurrencies()); currencyTextFieldBox.setVisible(!paymentAccount.hasMultipleCurrencies()); + + model.onPaymentAccountSelected(paymentAccount); + model.onCurrencySelected(model.getTradeCurrency()); + if (paymentAccount.hasMultipleCurrencies()) { final List tradeCurrencies = paymentAccount.getTradeCurrencies(); currencyComboBox.setItems(FXCollections.observableArrayList(tradeCurrencies)); - model.onPaymentAccountSelected(paymentAccount); + currencyComboBox.getSelectionModel().select(model.getTradeCurrency()); } else { TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); if (singleTradeCurrency != null) currencyTextField.setText(singleTradeCurrency.getNameAndCode()); - model.onPaymentAccountSelected(paymentAccount); - model.onCurrencySelected(model.getDataModel().getTradeCurrency()); } } else { currencySelection.setVisible(false); @@ -672,6 +714,15 @@ public abstract class MutableOfferView> exten marketBasedPriceTextField.clear(); volumeTextField.clear(); triggerPriceInputTextField.clear(); + if (!CurrencyUtil.isFiatCurrency(newValue)) { + if (model.isShownAsBuyOffer()) { + placeOfferButton.updateText(Res.get("createOffer.placeOfferButtonAltcoin", Res.get("shared.buy"), + model.getTradeCurrency().getCode())); + } else { + placeOfferButton.updateText(Res.get("createOffer.placeOfferButtonAltcoin", Res.get("shared.sell"), + model.getTradeCurrency().getCode())); + } + } }; placeOfferCompletedListener = (o, oldValue, newValue) -> { @@ -685,18 +736,12 @@ public abstract class MutableOfferView> exten .feedback(Res.get("createOffer.success.info")) .dontShowAgainId(key) .actionButtonTextWithGoTo("navigation.portfolio.myOpenOffers") - .onAction(() -> { - UserThread.runAfter(() -> - navigation.navigateTo(MainView.class, PortfolioView.class, - OpenOffersView.class), - 100, TimeUnit.MILLISECONDS); - close(); - }) - .onClose(this::close) + .onAction(this::closeAndGoToOpenOffers) + .onClose(this::closeAndGoToOpenOffers) .show(), 1); } else { - close(); + closeAndGoToOpenOffers(); } } }; @@ -789,11 +834,20 @@ public abstract class MutableOfferView> exten }); } + private void closeAndGoToOpenOffers() { + //go to open offers + UserThread.runAfter(() -> + navigation.navigateTo(MainView.class, PortfolioView.class, + OpenOffersView.class), + 1, TimeUnit.SECONDS); + close(); + } + protected void updatePriceToggle() { int marketPriceAvailableValue = model.marketPriceAvailableProperty.get(); if (marketPriceAvailableValue > -1) { boolean showPriceToggle = marketPriceAvailableValue == 1 && - !model.getDataModel().paymentAccount.isHalCashAccount(); + !model.getDataModel().paymentAccount.hasPaymentMethodWithId(HAL_CASH_ID); percentagePriceBox.setVisible(showPriceToggle); priceTypeToggleButton.setVisible(showPriceToggle); boolean fixedPriceSelected = !model.getDataModel().getUseMarketBasedPrice().get() || !showPriceToggle; @@ -871,15 +925,7 @@ public abstract class MutableOfferView> exten /////////////////////////////////////////////////////////////////////////////////////////// private void addScrollPane() { - scrollPane = new ScrollPane(); - scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - scrollPane.setFitToWidth(true); - scrollPane.setFitToHeight(true); - AnchorPane.setLeftAnchor(scrollPane, 0d); - AnchorPane.setTopAnchor(scrollPane, 0d); - AnchorPane.setRightAnchor(scrollPane, 0d); - AnchorPane.setBottomAnchor(scrollPane, 0d); + scrollPane = GUIUtil.createScrollPane(); root.getChildren().add(scrollPane); } @@ -889,18 +935,12 @@ public abstract class MutableOfferView> exten gridPane.setPadding(new Insets(30, 25, -1, 25)); gridPane.setHgap(5); gridPane.setVgap(5); - ColumnConstraints columnConstraints1 = new ColumnConstraints(); - columnConstraints1.setHalignment(HPos.RIGHT); - columnConstraints1.setHgrow(Priority.NEVER); - columnConstraints1.setMinWidth(200); - ColumnConstraints columnConstraints2 = new ColumnConstraints(); - columnConstraints2.setHgrow(Priority.ALWAYS); - gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); + GUIUtil.setDefaultTwoColumnConstraintsForGridPane(gridPane); scrollPane.setContent(gridPane); } private void addPaymentGroup() { - paymentTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 1, Res.get("shared.selectTradingAccount")); + paymentTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 1, Res.get("offerbook.createOffer")); GridPane.setColumnSpan(paymentTitledGroupBg, 2); HBox paymentGroupBox = new HBox(); @@ -909,7 +949,7 @@ public abstract class MutableOfferView> exten paymentGroupBox.setPadding(new Insets(10, 0, 18, 0)); final Tuple3> tradingAccountBoxTuple = addTopLabelComboBox( - Res.get("shared.tradingAccount"), Res.get("shared.selectTradingAccount")); + Res.get("shared.chooseTradingAccount"), Res.get("shared.chooseTradingAccount")); final Tuple3> currencyBoxTuple = addTopLabelComboBox( Res.get("shared.currency"), Res.get("list.currency.select")); @@ -969,12 +1009,15 @@ public abstract class MutableOfferView> exten advancedOptionsBox.setSpacing(40); GridPane.setRowIndex(advancedOptionsBox, gridRow); + GridPane.setColumnSpan(advancedOptionsBox, GridPane.REMAINING); GridPane.setColumnIndex(advancedOptionsBox, 0); GridPane.setHalignment(advancedOptionsBox, HPos.LEFT); GridPane.setMargin(advancedOptionsBox, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(advancedOptionsBox); - advancedOptionsBox.getChildren().addAll(getBuyerSecurityDepositBox(), getTradeFeeFieldsBox()); + VBox tradeFeeFieldsBox = getTradeFeeFieldsBox(); + tradeFeeFieldsBox.setMinWidth(240); + advancedOptionsBox.getChildren().addAll(getBuyerSecurityDepositBox(), tradeFeeFieldsBox); Tuple2 tuple = add2ButtonsAfterGroup(gridPane, ++gridRow, Res.get("shared.nextStep"), Res.get("shared.cancel")); @@ -1070,7 +1113,7 @@ public abstract class MutableOfferView> exten fundingHBox.setVisible(false); fundingHBox.setManaged(false); fundingHBox.setSpacing(10); - Button fundFromSavingsWalletButton = new AutoTooltipButton(Res.get("shared.fundFromSavingsWalletButton")); + fundFromSavingsWalletButton = new AutoTooltipButton(Res.get("shared.fundFromSavingsWalletButton")); fundFromSavingsWalletButton.setDefaultButton(true); fundFromSavingsWalletButton.getStyleClass().add("action-button"); fundFromSavingsWalletButton.setOnAction(e -> model.fundFromSavingsWallet()); @@ -1330,7 +1373,13 @@ public abstract class MutableOfferView> exten vBox.setAlignment(Pos.CENTER_LEFT); vBox.getChildren().addAll(tradeFeeInBtcLabel); - final Tuple2 tradeInputBox = getTradeInputBox(vBox, Res.get("createOffer.tradeFee.description")); + HBox hBox = new HBox(); + hBox.getChildren().addAll(vBox); + hBox.setMinHeight(47); + hBox.setMaxHeight(hBox.getMinHeight()); + HBox.setHgrow(vBox, Priority.ALWAYS); + + final Tuple2 tradeInputBox = getTradeInputBox(hBox, Res.get("createOffer.tradeFee.description")); tradeFeeDescriptionLabel = tradeInputBox.first; @@ -1349,30 +1398,34 @@ public abstract class MutableOfferView> exten infoGridPane.setPadding(new Insets(10, 10, 10, 10)); int i = 0; - if (model.isSellOffer()) - addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.tradeAmount"), model.tradeAmount.get()); + if (model.isSellOffer()) { + addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.tradeAmount"), model.getTradeAmount()); + } addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.yourSecurityDeposit"), model.getSecurityDepositInfo()); - addPayInfoEntry(infoGridPane, i++, Res.get("createOffer.fundsBox.offerFee"), model.getTradeFee()); - addPayInfoEntry(infoGridPane, i++, Res.get("createOffer.fundsBox.networkFee"), model.getTxFee()); + addPayInfoEntry(infoGridPane, i++, Res.getWithCol("createOffer.fundsBox.offerFee"), model.getTradeFee()); + addPayInfoEntry(infoGridPane, i++, Res.getWithCol("createOffer.fundsBox.networkFee"), model.getTxFee()); Separator separator = new Separator(); separator.setOrientation(Orientation.HORIZONTAL); separator.getStyleClass().add("offer-separator"); GridPane.setConstraints(separator, 1, i++); infoGridPane.getChildren().add(separator); - addPayInfoEntry(infoGridPane, i, Res.getWithCol("shared.total"), model.getTotalToPayInfo()); + addPayInfoEntry(infoGridPane, i, Res.getWithCol("shared.total"), + model.getTotalToPayInfo()); return infoGridPane; } - private void addPayInfoEntry(GridPane infoGridPane, int row, String labelText, String value) { - Label label = new AutoTooltipLabel(labelText); - TextField textField = new TextField(value); - textField.setMinWidth(500); - textField.setEditable(false); - textField.setFocusTraversable(false); - textField.setId("payment-info"); - GridPane.setConstraints(label, 0, row, 1, 1, HPos.RIGHT, VPos.CENTER); - GridPane.setConstraints(textField, 1, row); - infoGridPane.getChildren().addAll(label, textField); + /////////////////////////////////////////////////////////////////////////////////////////// + // Helpers + /////////////////////////////////////////////////////////////////////////////////////////// + + private ObservableList getPaymentAccounts() { + return filterPaymentAccounts(model.getDataModel().getPaymentAccounts()); } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract Methods + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract ObservableList filterPaymentAccounts(ObservableList paymentAccounts); } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java index 21b6409a65..84e8b00cd5 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java @@ -40,6 +40,7 @@ import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferRestrictions; import bisq.core.offer.OfferUtil; @@ -124,6 +125,7 @@ public abstract class MutableOfferViewModel ext public final StringProperty triggerPrice = new SimpleStringProperty(""); final StringProperty tradeFee = new SimpleStringProperty(); final StringProperty tradeFeeInBtcWithFiat = new SimpleStringProperty(); + final StringProperty tradeFeeCurrencyCode = new SimpleStringProperty(); final StringProperty tradeFeeDescription = new SimpleStringProperty(); final BooleanProperty isTradeFeeVisible = new SimpleBooleanProperty(false); @@ -181,6 +183,7 @@ public abstract class MutableOfferViewModel ext final IntegerProperty marketPriceAvailableProperty = new SimpleIntegerProperty(-1); private ChangeListener currenciesUpdateListener; protected boolean syncMinAmountWithAmount = true; + private boolean makeOfferFromUnsignedAccountWarningDisplayed; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -256,13 +259,17 @@ public abstract class MutableOfferViewModel ext } private void addBindings() { - if (dataModel.getDirection() == OfferPayload.Direction.BUY) { + if (dataModel.getDirection() == OfferDirection.BUY) { volumeDescriptionLabel.bind(createStringBinding( - () -> Res.get("createOffer.amountPriceBox.buy.volumeDescription", dataModel.getTradeCurrencyCode().get()), + () -> Res.get(CurrencyUtil.isFiatCurrency(dataModel.getTradeCurrencyCode().get()) ? + "createOffer.amountPriceBox.buy.volumeDescription" : + "createOffer.amountPriceBox.buy.volumeDescriptionAltcoin", dataModel.getTradeCurrencyCode().get()), dataModel.getTradeCurrencyCode())); } else { volumeDescriptionLabel.bind(createStringBinding( - () -> Res.get("createOffer.amountPriceBox.sell.volumeDescription", dataModel.getTradeCurrencyCode().get()), + () -> Res.get(CurrencyUtil.isFiatCurrency(dataModel.getTradeCurrencyCode().get()) ? + "createOffer.amountPriceBox.sell.volumeDescription" : + "createOffer.amountPriceBox.sell.volumeDescriptionAltcoin", dataModel.getTradeCurrencyCode().get()), dataModel.getTradeCurrencyCode())); } volumePromptLabel.bind(createStringBinding( @@ -325,12 +332,12 @@ public abstract class MutableOfferViewModel ext try { double priceAsDouble = ParsingUtils.parseNumberStringToDouble(price.get()); double relation = priceAsDouble / marketPriceAsDouble; - final OfferPayload.Direction compareDirection = CurrencyUtil.isCryptoCurrency(currencyCode) ? - OfferPayload.Direction.SELL : - OfferPayload.Direction.BUY; + final OfferDirection compareDirection = CurrencyUtil.isCryptoCurrency(currencyCode) ? + OfferDirection.SELL : + OfferDirection.BUY; double percentage = dataModel.getDirection() == compareDirection ? 1 - relation : relation - 1; percentage = MathUtils.roundDouble(percentage, 4); - dataModel.setMarketPriceMargin(percentage); + dataModel.setMarketPriceMarginPct(percentage); marketPriceMargin.set(FormattingUtils.formatToPercent(percentage)); applyMakerFee(); } catch (NumberFormatException t) { @@ -352,7 +359,7 @@ public abstract class MutableOfferViewModel ext double percentage = ParsingUtils.parsePercentStringToDouble(newValue); if (percentage >= 1 || percentage <= -1) { new Popup().warning(Res.get("popup.warning.tooLargePercentageValue") + "\n" + - Res.get("popup.warning.examplePercentageValue")) + Res.get("popup.warning.examplePercentageValue")) .show(); } else { final String currencyCode = dataModel.getTradeCurrencyCode().get(); @@ -361,9 +368,9 @@ public abstract class MutableOfferViewModel ext percentage = MathUtils.roundDouble(percentage, 4); double marketPriceAsDouble = marketPrice.getPrice(); final boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode); - final OfferPayload.Direction compareDirection = isCryptoCurrency ? - OfferPayload.Direction.SELL : - OfferPayload.Direction.BUY; + final OfferDirection compareDirection = isCryptoCurrency ? + OfferDirection.SELL : + OfferDirection.BUY; double factor = dataModel.getDirection() == compareDirection ? 1 - percentage : 1 + percentage; @@ -375,7 +382,7 @@ public abstract class MutableOfferViewModel ext price.set(FormattingUtils.formatRoundedDoubleWithPrecision(targetPrice, precision)); ignorePriceStringListener = false; setPriceToModel(); - dataModel.setMarketPriceMargin(percentage); + dataModel.setMarketPriceMarginPct(percentage); dataModel.calculateVolume(); dataModel.calculateTotalToPay(); updateButtonDisableState(); @@ -459,7 +466,7 @@ public abstract class MutableOfferViewModel ext volumeListener = (ov, oldValue, newValue) -> { ignoreVolumeStringListener = true; if (newValue != null) - volume.set(DisplayUtils.formatVolume(newValue)); + volume.set(VolumeUtil.formatVolume(newValue)); else volume.set(""); @@ -470,8 +477,10 @@ public abstract class MutableOfferViewModel ext securityDepositAsDoubleListener = (ov, oldValue, newValue) -> { if (newValue != null) { buyerSecurityDeposit.set(FormattingUtils.formatToPercent((double) newValue)); - if (dataModel.getAmount().get() != null) + if (dataModel.getAmount().get() != null) { buyerSecurityDepositInBTC.set(btcFormatter.formatCoinWithCode(dataModel.getBuyerSecurityDepositAsCoin())); + } + updateBuyerSecurityDeposit(); } else { buyerSecurityDeposit.set(""); buyerSecurityDepositInBTC.set(""); @@ -491,6 +500,7 @@ public abstract class MutableOfferViewModel ext } private void applyMakerFee() { + tradeFeeCurrencyCode.set(Res.getBaseCurrencyCode()); tradeFeeDescription.set(Res.get("createOffer.tradeFee.descriptionBTCOnly")); Coin makerFeeAsCoin = dataModel.getMakerFee(); @@ -500,7 +510,7 @@ public abstract class MutableOfferViewModel ext isTradeFeeVisible.setValue(true); tradeFee.set(getFormatterForMakerFee().formatCoin(makerFeeAsCoin)); - tradeFeeInBtcWithFiat.set(FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil, + tradeFeeInBtcWithFiat.set(OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getMakerFeeInBtc(), btcFormatter)); } @@ -566,16 +576,24 @@ public abstract class MutableOfferViewModel ext // API /////////////////////////////////////////////////////////////////////////////////////////// - boolean initWithData(OfferPayload.Direction direction, TradeCurrency tradeCurrency) { + boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) { boolean result = dataModel.initWithData(direction, tradeCurrency); if (dataModel.paymentAccount != null) btcValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimitAsCoin(dataModel.getTradeCurrencyCode().get())); btcValidator.setMaxTradeLimit(Coin.valueOf(dataModel.getMaxTradeLimit())); btcValidator.setMinValue(Restrictions.getMinTradeAmount()); - final boolean isBuy = dataModel.getDirection() == OfferPayload.Direction.BUY; - amountDescription = Res.get("createOffer.amountPriceBox.amountDescription", - isBuy ? Res.get("shared.buy") : Res.get("shared.sell")); + final boolean isBuy = dataModel.getDirection() == OfferDirection.BUY; + + boolean isFiatCurrency = CurrencyUtil.isFiatCurrency(tradeCurrency.getCode()); + + if (isFiatCurrency) { + amountDescription = Res.get("createOffer.amountPriceBox.amountDescription", + isBuy ? Res.get("shared.buy") : Res.get("shared.sell")); + } else { + amountDescription = Res.get(isBuy ? "createOffer.amountPriceBox.sell.amountDescriptionAltcoin" : + "createOffer.amountPriceBox.buy.amountDescriptionAltcoin"); + } securityDepositValidator.setPaymentAccount(dataModel.paymentAccount); validateAndSetBuyerSecurityDepositToModel(); @@ -663,19 +681,17 @@ public abstract class MutableOfferViewModel ext updateSpinnerInfo(); } - boolean fundFromSavingsWallet() { + void fundFromSavingsWallet() { dataModel.fundFromSavingsWallet(); if (dataModel.getIsBtcWalletFunded().get()) { updateButtonDisableState(); - return true; } else { new Popup().warning(Res.get("shared.notEnoughFunds", - btcFormatter.formatCoinWithCode(dataModel.totalToPayAsCoinProperty().get()), - btcFormatter.formatCoinWithCode(dataModel.getTotalAvailableBalance()))) + btcFormatter.formatCoinWithCode(dataModel.totalToPayAsCoinProperty().get()), + btcFormatter.formatCoinWithCode(dataModel.getTotalAvailableBalance()))) .actionButtonTextWithGoTo("navigation.funds.depositFunds") .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class)) .show(); - return false; } } @@ -706,8 +722,8 @@ public abstract class MutableOfferViewModel ext } else if (amount.get() != null && btcValidator.getMaxTradeLimit() != null && btcValidator.getMaxTradeLimit().value == OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value) { amount.set(btcFormatter.formatCoin(btcValidator.getMaxTradeLimit())); new Popup().information(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.buyer", - btcFormatter.formatCoinWithCode(getEffectiveLimit()), - Res.get("offerbook.warning.newVersionAnnouncement"))) + btcFormatter.formatCoinWithCode(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT), + Res.get("offerbook.warning.newVersionAnnouncement"))) .width(900) .show(); } @@ -723,10 +739,6 @@ public abstract class MutableOfferViewModel ext } } - private Coin getEffectiveLimit() { - return btcValidator.getMaxValue() == null ? btcValidator.getMaxTradeLimit() : btcValidator.getMaxValue(); - } - public void onFocusOutMinAmountTextField(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { InputValidator.ValidationResult result = isBtcInputValid(minAmount.get()); @@ -740,7 +752,7 @@ public abstract class MutableOfferViewModel ext if (dataModel.getMinVolume().get() != null) { InputValidator.ValidationResult minVolumeResult = isVolumeInputValid( - DisplayUtils.formatVolume(dataModel.getMinVolume().get())); + VolumeUtil.formatVolume(dataModel.getMinVolume().get())); volumeValidationResult.set(minVolumeResult); @@ -777,12 +789,14 @@ public abstract class MutableOfferViewModel ext // if not reset here. Not clear why... triggerPriceValidationResult.set(new InputValidator.ValidationResult(true)); - if (dataModel.getPrice().get() == null) // fix NPE @ bisq/issues/5166 - return; + String currencyCode = dataModel.getTradeCurrencyCode().get(); + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + InputValidator.ValidationResult result = PriceUtil.isTriggerPriceValid(triggerPriceAsString, - dataModel.getPrice().get(), + marketPrice, dataModel.isSellOffer(), - dataModel.isFiatCurrency()); + dataModel.isFiatCurrency() + ); triggerPriceValidationResult.set(result); updateButtonDisableState(); if (result.isValid) { @@ -798,6 +812,7 @@ public abstract class MutableOfferViewModel ext } void onFixPriceToggleChange(boolean fixedPriceSelected) { + inputIsMarketBasedPrice = !fixedPriceSelected; updateButtonDisableState(); if (!fixedPriceSelected) { onTriggerPriceTextFieldChanged(); @@ -839,7 +854,7 @@ public abstract class MutableOfferViewModel ext // field wasn't set manually inputIsMarketBasedPrice = true; } - marketPriceMargin.set(FormattingUtils.formatRoundedDoubleWithPrecision(dataModel.getMarketPriceMargin() * 100, 2)); + marketPriceMargin.set(FormattingUtils.formatRoundedDoubleWithPrecision(dataModel.getMarketPriceMarginPct() * 100, 2)); } // We want to trigger a recalculation of the volume, as well as update trigger price validation @@ -860,12 +875,12 @@ public abstract class MutableOfferViewModel ext Volume volume = dataModel.getVolume().get(); if (volume != null) { // For HalCash we want multiple of 10 EUR - if (dataModel.paymentAccount.isHalCashAccount()) + if (dataModel.isUsingHalCashAccount()) volume = VolumeUtil.getAdjustedVolumeForHalCash(volume); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) volume = VolumeUtil.getRoundedFiatVolume(volume); - this.volume.set(DisplayUtils.formatVolume(volume)); + this.volume.set(VolumeUtil.formatVolume(volume)); } ignoreVolumeStringListener = false; @@ -952,7 +967,7 @@ public abstract class MutableOfferViewModel ext private void displayPriceOutOfRangePopup() { Popup popup = new Popup(); popup.warning(Res.get("createOffer.priceOutSideOfDeviation", - FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent()))) + FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent()))) .actionButtonText(Res.get("createOffer.changePrice")) .onAction(popup::hide) .closeButtonTextWithGoTo("navigation.settings.preferences") @@ -964,8 +979,12 @@ public abstract class MutableOfferViewModel ext return btcFormatter; } + public boolean isShownAsBuyOffer() { + return OfferViewUtil.isShownAsBuyOffer(dataModel.getDirection(), dataModel.getTradeCurrency()); + } + public boolean isSellOffer() { - return dataModel.getDirection() == OfferPayload.Direction.SELL; + return dataModel.getDirection() == OfferDirection.SELL; } public TradeCurrency getTradeCurrency() { @@ -973,7 +992,9 @@ public abstract class MutableOfferViewModel ext } public String getTradeAmount() { - return btcFormatter.formatCoinWithCode(dataModel.getAmount().get()); + return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, + dataModel.getAmount().get(), + btcFormatter); } public String getSecurityDepositLabel() { @@ -987,10 +1008,12 @@ public abstract class MutableOfferViewModel ext } public String getSecurityDepositInfo() { - return btcFormatter.formatCoinWithCode(dataModel.getSecurityDeposit()) + - GUIUtil.getPercentageOfTradeAmount(dataModel.getSecurityDeposit(), - dataModel.getAmount().get(), - Restrictions.getMinBuyerSecurityDepositAsCoin()); + return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, + dataModel.getSecurityDeposit(), + dataModel.getAmount().get(), + btcFormatter, + Restrictions.getMinBuyerSecurityDepositAsCoin() + ); } public String getSecurityDepositWithCode() { @@ -999,7 +1022,7 @@ public abstract class MutableOfferViewModel ext public String getTradeFee() { - return FeeUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, + return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getMakerFeeInBtc(), dataModel.getAmount().get(), btcFormatter, @@ -1012,8 +1035,9 @@ public abstract class MutableOfferViewModel ext } public String getTotalToPayInfo() { - final String totalToPay = this.totalToPay.get(); - return totalToPay; + return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, + dataModel.totalToPayAsCoin.get(), + btcFormatter); } public String getFundsStructure() { @@ -1024,7 +1048,7 @@ public abstract class MutableOfferViewModel ext } public String getTxFee() { - return FeeUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, + return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getTxFee(), dataModel.getAmount().get(), btcFormatter, @@ -1104,8 +1128,8 @@ public abstract class MutableOfferViewModel ext long maxTradeLimit = dataModel.getMaxTradeLimit(); Price price = dataModel.getPrice().get(); - if (price != null) { - if (dataModel.paymentAccount.isHalCashAccount()) + if (price != null && price.isPositive()) { + if (dataModel.isUsingHalCashAccount()) amount = CoinUtil.getAdjustedAmountForHalCash(amount, price, maxTradeLimit); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) amount = CoinUtil.getRoundedFiatAmount(amount, price, maxTradeLimit); @@ -1128,8 +1152,8 @@ public abstract class MutableOfferViewModel ext Price price = dataModel.getPrice().get(); long maxTradeLimit = dataModel.getMaxTradeLimit(); - if (price != null) { - if (dataModel.paymentAccount.isHalCashAccount()) + if (price != null && price.isPositive()) { + if (dataModel.isUsingHalCashAccount()) minAmount = CoinUtil.getAdjustedAmountForHalCash(minAmount, price, maxTradeLimit); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) minAmount = CoinUtil.getRoundedFiatAmount(minAmount, price, maxTradeLimit); @@ -1182,11 +1206,13 @@ public abstract class MutableOfferViewModel ext } private void maybeShowMakeOfferToUnsignedAccountWarning() { - if (dataModel.getDirection() == OfferPayload.Direction.SELL && + if (!makeOfferFromUnsignedAccountWarningDisplayed && + dataModel.getDirection() == OfferDirection.SELL && PaymentMethod.hasChargebackRisk(dataModel.getPaymentAccount().getPaymentMethod(), dataModel.getTradeCurrency().getCode())) { Coin checkAmount = dataModel.getMinAmount().get() == null ? dataModel.getAmount().get() : dataModel.getMinAmount().get(); if (checkAmount != null && !checkAmount.isGreaterThan(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT)) { - GUIUtil.showMakeOfferToUnsignedAccountWarning(btcFormatter); + makeOfferFromUnsignedAccountWarningDisplayed = true; + GUIUtil.showMakeOfferToUnsignedAccountWarning(); } } } @@ -1223,11 +1249,6 @@ public abstract class MutableOfferViewModel ext waitingForFundsText.set(""); } else if (dataModel.getIsBtcWalletFunded().get()) { waitingForFundsText.set(""); - /* if (dataModel.isFeeFromFundingTxSufficient.get()) { - spinnerInfoText.set(""); - } else { - spinnerInfoText.set("Check if funding tx miner fee is sufficient..."); - }*/ } else { waitingForFundsText.set(Res.get("shared.waitingForFunds")); } @@ -1254,7 +1275,7 @@ public abstract class MutableOfferViewModel ext dataModel.getPrice().get() != null && dataModel.getPrice().get().getValue() != 0 && isVolumeInputValid(volume.get()).isValid && - isVolumeInputValid(DisplayUtils.formatVolume(dataModel.getMinVolume().get())).isValid && + isVolumeInputValid(VolumeUtil.formatVolume(dataModel.getMinVolume().get())).isValid && dataModel.isMinAmountLessOrEqualAmount(); if (dataModel.useMarketBasedPrice.get() && dataModel.isMarketPriceAvailable()) { @@ -1267,8 +1288,6 @@ public abstract class MutableOfferViewModel ext } isNextButtonDisabled.set(!inputDataValid); - // boolean notSufficientFees = dataModel.isWalletFunded.get() && dataModel.isMainNet.get() && !dataModel.isFeeFromFundingTxSufficient.get(); - //isPlaceOfferButtonDisabled.set(createOfferRequested || !inputDataValid || notSufficientFees); isPlaceOfferButtonDisabled.set(createOfferRequested || !inputDataValid || !dataModel.getIsBtcWalletFunded().get()); } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/OfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/OfferView.java index d9d9857da3..3903452dc4 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/OfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/OfferView.java @@ -23,19 +23,20 @@ import bisq.desktop.common.view.View; import bisq.desktop.common.view.ViewLoader; import bisq.desktop.main.MainView; import bisq.desktop.main.offer.createoffer.CreateOfferView; +import bisq.desktop.main.offer.offerbook.BtcOfferBookView; import bisq.desktop.main.offer.offerbook.OfferBookView; +import bisq.desktop.main.offer.offerbook.OtherOfferBookView; +import bisq.desktop.main.offer.offerbook.TopAltcoinOfferBookView; import bisq.desktop.main.offer.takeoffer.TakeOfferView; -import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.GUIUtil; - +import bisq.common.UserThread; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.GlobalSettings; -import bisq.core.locale.LanguageUtil; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; -import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.offer.OfferDirection; +import bisq.core.payment.payload.PaymentMethod; import bisq.core.user.Preferences; import bisq.core.user.User; @@ -43,249 +44,302 @@ import bisq.network.p2p.P2PService; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; -import javafx.scene.layout.AnchorPane; import javafx.beans.value.ChangeListener; -import javafx.collections.ListChangeListener; - -import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; public abstract class OfferView extends ActivatableView { - private OfferBookView offerBookView; - private CreateOfferView createOfferView; - private TakeOfferView takeOfferView; - private AnchorPane createOfferPane, takeOfferPane; - private Tab takeOfferTab, createOfferTab, offerBookTab; + private OfferBookView btcOfferBookView, topAltcoinOfferBookView, otherOfferBookView; + + private Tab btcOfferBookTab, topAltcoinOfferBookTab, otherOfferBookTab; private final ViewLoader viewLoader; private final Navigation navigation; private final Preferences preferences; private final User user; private final P2PService p2PService; - private final OfferPayload.Direction direction; - private final ArbitratorManager arbitratorManager; + private final OfferDirection direction; private Offer offer; private TradeCurrency tradeCurrency; - private boolean createOfferViewOpen, takeOfferViewOpen; private Navigation.Listener navigationListener; private ChangeListener tabChangeListener; - private ListChangeListener tabListChangeListener; + private OfferView.OfferActionHandler offerActionHandler; protected OfferView(ViewLoader viewLoader, Navigation navigation, Preferences preferences, - ArbitratorManager arbitratorManager, User user, P2PService p2PService, - OfferPayload.Direction direction) { + OfferDirection direction) { this.viewLoader = viewLoader; this.navigation = navigation; this.preferences = preferences; this.user = user; this.p2PService = p2PService; this.direction = direction; - this.arbitratorManager = arbitratorManager; } @Override protected void initialize() { navigationListener = (viewPath, data) -> { - if (viewPath.size() == 3 && viewPath.indexOf(this.getClass()) == 1) - loadView(viewPath.tip()); + UserThread.execute(() -> { + if (viewPath.size() == 3 && viewPath.indexOf(this.getClass()) == 1) { + loadView(viewPath.tip(), null, data); + } else if (viewPath.size() == 4 && viewPath.indexOf(this.getClass()) == 1) { + loadView(viewPath.get(2), viewPath.tip(), data); + } + }); }; tabChangeListener = (observableValue, oldValue, newValue) -> { - if (newValue != null) { - if (newValue.equals(createOfferTab) && createOfferView != null) { - createOfferView.onTabSelected(true); - } else if (newValue.equals(takeOfferTab) && takeOfferView != null) { - takeOfferView.onTabSelected(true); - } else if (newValue.equals(offerBookTab) && offerBookView != null) { - offerBookView.onTabSelected(true); + UserThread.execute(() -> { + if (newValue != null) { + if (newValue.equals(btcOfferBookTab)) { + if (btcOfferBookView != null) { + btcOfferBookView.onTabSelected(true); + } else { + loadView(BtcOfferBookView.class, null, null); + } + } else if (newValue.equals(topAltcoinOfferBookTab)) { + if (topAltcoinOfferBookView != null) { + topAltcoinOfferBookView.onTabSelected(true); + } else { + loadView(TopAltcoinOfferBookView.class, null, null); + } + } else if (newValue.equals(otherOfferBookTab)) { + if (otherOfferBookView != null) { + otherOfferBookView.onTabSelected(true); + } else { + loadView(OtherOfferBookView.class, null, null); + } + } } - } - if (oldValue != null) { - if (oldValue.equals(createOfferTab) && createOfferView != null) { - createOfferView.onTabSelected(false); - } else if (oldValue.equals(takeOfferTab) && takeOfferView != null) { - takeOfferView.onTabSelected(false); - } else if (oldValue.equals(offerBookTab) && offerBookView != null) { - offerBookView.onTabSelected(false); + if (oldValue != null) { + if (oldValue.equals(btcOfferBookTab) && btcOfferBookView != null) { + btcOfferBookView.onTabSelected(false); + } else if (oldValue.equals(topAltcoinOfferBookTab) && topAltcoinOfferBookView != null) { + topAltcoinOfferBookView.onTabSelected(false); + } else if (oldValue.equals(otherOfferBookTab) && otherOfferBookView != null) { + otherOfferBookView.onTabSelected(false); + } } - } + }); }; - tabListChangeListener = change -> { - change.next(); - List removedTabs = change.getRemoved(); - if (removedTabs.size() == 1) { - if (removedTabs.get(0).getContent().equals(createOfferPane)) - onCreateOfferViewRemoved(); - else if (removedTabs.get(0).getContent().equals(takeOfferPane)) - onTakeOfferViewRemoved(); + + offerActionHandler = new OfferActionHandler() { + @Override + public void onCreateOffer(TradeCurrency tradeCurrency, PaymentMethod paymentMethod) { + if (canCreateOrTakeOffer(tradeCurrency)) { + showCreateOffer(tradeCurrency, paymentMethod); + } + } + + @Override + public void onTakeOffer(Offer offer) { + Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(offer.getCurrencyCode()); + if (optionalTradeCurrency.isPresent() && canCreateOrTakeOffer(optionalTradeCurrency.get())) { + showTakeOffer(offer); + } } }; } @Override protected void activate() { - Optional tradeCurrencyOptional = (this.direction == OfferPayload.Direction.SELL) ? + Optional tradeCurrencyOptional = (this.direction == OfferDirection.SELL) ? CurrencyUtil.getTradeCurrency(preferences.getSellScreenCurrencyCode()) : CurrencyUtil.getTradeCurrency(preferences.getBuyScreenCurrencyCode()); tradeCurrency = tradeCurrencyOptional.orElseGet(GlobalSettings::getDefaultTradeCurrency); root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); - root.getTabs().addListener(tabListChangeListener); navigation.addListener(navigationListener); - navigation.navigateTo(MainView.class, this.getClass(), OfferBookView.class); + if (btcOfferBookView == null) { + navigation.navigateTo(MainView.class, this.getClass(), BtcOfferBookView.class); + } + + GUIUtil.updateTopAltcoin(preferences); + + if (topAltcoinOfferBookTab != null) { + topAltcoinOfferBookTab.setText(GUIUtil.TOP_ALTCOIN.getCode()); + } } @Override protected void deactivate() { navigation.removeListener(navigationListener); root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); - root.getTabs().removeListener(tabListChangeListener); } - private String getCreateOfferTabName() { - return Res.get("offerbook.createOffer").toUpperCase(); - } - - private String getTakeOfferTabName() { - return Res.get("offerbook.takeOffer").toUpperCase(); - } - - private void loadView(Class viewClass) { + private void loadView(Class viewClass, + Class childViewClass, + @Nullable Object data) { TabPane tabPane = root; tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.ALL_TABS); - View view; - boolean isBuy = direction == OfferPayload.Direction.BUY; - if (viewClass == OfferBookView.class && offerBookView == null) { - view = viewLoader.load(viewClass); - // Offerbook must not be cached by ViewLoader as we use 2 instances for sell and buy screens. - offerBookTab = new Tab(isBuy ? Res.get("shared.buyBitcoin").toUpperCase() : Res.get("shared.sellBitcoin").toUpperCase()); - offerBookTab.setClosable(false); - offerBookTab.setContent(view.getRoot()); - tabPane.getTabs().add(offerBookTab); - offerBookView = (OfferBookView) view; - offerBookView.onTabSelected(true); + if (OfferBookView.class.isAssignableFrom(viewClass)) { - OfferActionHandler offerActionHandler = new OfferActionHandler() { - @Override - public void onCreateOffer(TradeCurrency tradeCurrency) { - if (createOfferViewOpen) { - tabPane.getTabs().remove(createOfferTab); - } - if (canCreateOrTakeOffer()) { - openCreateOffer(tradeCurrency); - } + if (viewClass == BtcOfferBookView.class && btcOfferBookTab != null && btcOfferBookView != null) { + if (childViewClass == null) { + btcOfferBookTab.setContent(btcOfferBookView.getRoot()); + } else if (childViewClass == TakeOfferView.class) { + loadTakeViewClass(viewClass, childViewClass, btcOfferBookTab); + } else { + loadCreateViewClass(btcOfferBookView, viewClass, childViewClass, btcOfferBookTab, (PaymentMethod) data); } - - @Override - public void onTakeOffer(Offer offer) { - if (takeOfferViewOpen) { - tabPane.getTabs().remove(takeOfferTab); - } - if (canCreateOrTakeOffer()) { - openTakeOffer(offer); - } + tabPane.getSelectionModel().select(btcOfferBookTab); + } else if (viewClass == TopAltcoinOfferBookView.class && topAltcoinOfferBookTab != null && topAltcoinOfferBookView != null) { + if (childViewClass == null) { + topAltcoinOfferBookTab.setContent(topAltcoinOfferBookView.getRoot()); + } else if (childViewClass == TakeOfferView.class) { + loadTakeViewClass(viewClass, childViewClass, topAltcoinOfferBookTab); + } else { + tradeCurrency = GUIUtil.TOP_ALTCOIN; + loadCreateViewClass(topAltcoinOfferBookView, viewClass, childViewClass, topAltcoinOfferBookTab, (PaymentMethod) data); } - }; - offerBookView.setOfferActionHandler(offerActionHandler); - offerBookView.setDirection(direction); - } else if (viewClass == CreateOfferView.class && createOfferView == null) { - view = viewLoader.load(viewClass); - // CreateOffer and TakeOffer must not be cached by ViewLoader as we cannot use a view multiple times - // in different graphs - createOfferView = (CreateOfferView) view; - createOfferView.initWithData(direction, tradeCurrency); - createOfferPane = createOfferView.getRoot(); - createOfferTab = new Tab(getCreateOfferTabName()); - createOfferTab.setClosable(true); - // close handler from close on create offer action - createOfferView.setCloseHandler(() -> tabPane.getTabs().remove(createOfferTab)); - createOfferTab.setContent(createOfferPane); - tabPane.getTabs().add(createOfferTab); - tabPane.getSelectionModel().select(createOfferTab); - } else if (viewClass == TakeOfferView.class && takeOfferView == null && offer != null) { - view = viewLoader.load(viewClass); - // CreateOffer and TakeOffer must not be cached by ViewLoader as we cannot use a view multiple times - // in different graphs - takeOfferView = (TakeOfferView) view; - takeOfferView.initWithData(offer); - takeOfferPane = ((TakeOfferView) view).getRoot(); - takeOfferTab = new Tab(getTakeOfferTabName()); - takeOfferTab.setClosable(true); - // close handler from close on take offer action - takeOfferView.setCloseHandler(() -> tabPane.getTabs().remove(takeOfferTab)); - takeOfferTab.setContent(takeOfferPane); - tabPane.getTabs().add(takeOfferTab); - tabPane.getSelectionModel().select(takeOfferTab); + tabPane.getSelectionModel().select(topAltcoinOfferBookTab); + } else if (viewClass == OtherOfferBookView.class && otherOfferBookTab != null && otherOfferBookView != null) { + if (childViewClass == null) { + otherOfferBookTab.setContent(otherOfferBookView.getRoot()); + } else if (childViewClass == TakeOfferView.class) { + loadTakeViewClass(viewClass, childViewClass, otherOfferBookTab); + } else { + //add sanity check in case of app restart + if (CurrencyUtil.isFiatCurrency(tradeCurrency.getCode())) { + Optional tradeCurrencyOptional = (this.direction == OfferDirection.SELL) ? + CurrencyUtil.getTradeCurrency(preferences.getSellScreenCryptoCurrencyCode()) : + CurrencyUtil.getTradeCurrency(preferences.getBuyScreenCryptoCurrencyCode()); + tradeCurrency = tradeCurrencyOptional.isEmpty() ? OfferViewUtil.getAnyOfMainCryptoCurrencies() : tradeCurrencyOptional.get(); + } + loadCreateViewClass(otherOfferBookView, viewClass, childViewClass, otherOfferBookTab, (PaymentMethod) data); + } + tabPane.getSelectionModel().select(otherOfferBookTab); + } else { + if (btcOfferBookTab == null) { + btcOfferBookTab = new Tab(Res.getBaseCurrencyName().toUpperCase()); + btcOfferBookTab.setClosable(false); + topAltcoinOfferBookTab = new Tab(GUIUtil.TOP_ALTCOIN.getCode()); + topAltcoinOfferBookTab.setClosable(false); + otherOfferBookTab = new Tab(Res.get("shared.other").toUpperCase()); + otherOfferBookTab.setClosable(false); + + tabPane.getTabs().addAll(btcOfferBookTab, topAltcoinOfferBookTab, otherOfferBookTab); + } + if (viewClass == BtcOfferBookView.class) { + btcOfferBookView = (BtcOfferBookView) viewLoader.load(BtcOfferBookView.class); + btcOfferBookView.setOfferActionHandler(offerActionHandler); + btcOfferBookView.setDirection(direction); + btcOfferBookView.onTabSelected(true); + tabPane.getSelectionModel().select(btcOfferBookTab); + btcOfferBookTab.setContent(btcOfferBookView.getRoot()); + } else if (viewClass == TopAltcoinOfferBookView.class) { + topAltcoinOfferBookView = (TopAltcoinOfferBookView) viewLoader.load(TopAltcoinOfferBookView.class); + topAltcoinOfferBookView.setOfferActionHandler(offerActionHandler); + topAltcoinOfferBookView.setDirection(direction); + topAltcoinOfferBookView.onTabSelected(true); + tabPane.getSelectionModel().select(topAltcoinOfferBookTab); + topAltcoinOfferBookTab.setContent(topAltcoinOfferBookView.getRoot()); + } else if (viewClass == OtherOfferBookView.class) { + otherOfferBookView = (OtherOfferBookView) viewLoader.load(OtherOfferBookView.class); + otherOfferBookView.setOfferActionHandler(offerActionHandler); + otherOfferBookView.setDirection(direction); + otherOfferBookView.onTabSelected(true); + tabPane.getSelectionModel().select(otherOfferBookTab); + otherOfferBookTab.setContent(otherOfferBookView.getRoot()); + } + } } } - protected boolean canCreateOrTakeOffer() { + private void loadCreateViewClass(OfferBookView offerBookView, + Class viewClass, + Class childViewClass, + Tab marketOfferBookTab, + @Nullable PaymentMethod paymentMethod) { + if (tradeCurrency == null) { + return; + } + + View view; + // CreateOffer and TakeOffer must not be cached by ViewLoader as we cannot use a view multiple times + // in different graphs + view = viewLoader.load(childViewClass); + ((CreateOfferView) view).initWithData(direction, tradeCurrency, offerActionHandler); + + ((SelectableView) view).onTabSelected(true); + + ((ClosableView) view).setCloseHandler(() -> { + offerBookView.enableCreateOfferButton(); + ((SelectableView) view).onTabSelected(false); + //reset tab + navigation.navigateTo(MainView.class, this.getClass(), viewClass); + }); + + // close handler from close on create offer action + marketOfferBookTab.setContent(view.getRoot()); + } + + private void loadTakeViewClass(Class viewClass, + Class childViewClass, + Tab marketOfferBookTab) { + + if (offer == null) { + return; + } + + View view = viewLoader.load(childViewClass); + // CreateOffer and TakeOffer must not be cached by ViewLoader as we cannot use a view multiple times + // in different graphs + ((InitializableViewWithTakeOfferData) view).initWithData(offer); + ((SelectableView) view).onTabSelected(true); + + // close handler from close on take offer action + ((ClosableView) view).setCloseHandler(() -> { + ((SelectableView) view).onTabSelected(false); + navigation.navigateTo(MainView.class, this.getClass(), viewClass); + }); + marketOfferBookTab.setContent(view.getRoot()); + } + + protected boolean canCreateOrTakeOffer(TradeCurrency tradeCurrency) { return GUIUtil.isBootstrappedOrShowPopup(p2PService) && GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation); } - private void showNoArbitratorForUserLocaleWarning() { - String key = "NoArbitratorForUserLocaleWarning"; - new Popup().information(Res.get("offerbook.info.noArbitrationInUserLanguage", - getArbitrationLanguages(), LanguageUtil.getDisplayName(preferences.getUserLanguage()))) - .closeButtonText(Res.get("shared.ok")) - .dontShowAgainId(key) - .show(); + private void showTakeOffer(Offer offer) { + this.offer = offer; + + Class> offerBookViewClass = getOfferBookViewClassFor(offer.getCurrencyCode()); + navigation.navigateTo(MainView.class, this.getClass(), offerBookViewClass, TakeOfferView.class); } - private String getArbitrationLanguages() { - return arbitratorManager.getObservableMap().values().stream() - .flatMap(arbitrator -> arbitrator.getLanguageCodes().stream()) - .distinct() - .map(languageCode -> LanguageUtil.getDisplayName(languageCode)) - .collect(Collectors.joining(", ")); + private void showCreateOffer(TradeCurrency tradeCurrency, PaymentMethod paymentMethod) { + this.tradeCurrency = tradeCurrency; + + Class> offerBookViewClass = getOfferBookViewClassFor(tradeCurrency.getCode()); + navigation.navigateToWithData(paymentMethod, MainView.class, this.getClass(), offerBookViewClass, CreateOfferView.class); } - private void openTakeOffer(Offer offer) { - OfferView.this.takeOfferViewOpen = true; - OfferView.this.offer = offer; - OfferView.this.navigation.navigateTo(MainView.class, OfferView.this.getClass(), TakeOfferView.class); - } - - private void openCreateOffer(TradeCurrency tradeCurrency) { - OfferView.this.createOfferViewOpen = true; - OfferView.this.tradeCurrency = tradeCurrency; - OfferView.this.navigation.navigateTo(MainView.class, OfferView.this.getClass(), CreateOfferView.class); - } - - private void onCreateOfferViewRemoved() { - createOfferViewOpen = false; - if (createOfferView != null) { - createOfferView.onClose(); - createOfferView = null; + @NotNull + private Class> getOfferBookViewClassFor(String currencyCode) { + Class> offerBookViewClass; + if (CurrencyUtil.isFiatCurrency(currencyCode)) { + offerBookViewClass = BtcOfferBookView.class; + } else if (currencyCode.equals(GUIUtil.TOP_ALTCOIN.getCode())) { + offerBookViewClass = TopAltcoinOfferBookView.class; + } else { + offerBookViewClass = OtherOfferBookView.class; } - offerBookView.enableCreateOfferButton(); - - navigation.navigateTo(MainView.class, this.getClass(), OfferBookView.class); - } - - private void onTakeOfferViewRemoved() { - offer = null; - takeOfferViewOpen = false; - if (takeOfferView != null) { - takeOfferView.onClose(); - takeOfferView = null; - } - - navigation.navigateTo(MainView.class, this.getClass(), OfferBookView.class); + return offerBookViewClass; } public interface OfferActionHandler { - void onCreateOffer(TradeCurrency tradeCurrency); + void onCreateOffer(TradeCurrency tradeCurrency, PaymentMethod paymentMethod); void onTakeOffer(Offer offer); } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/FeeUtil.java b/desktop/src/main/java/bisq/desktop/main/offer/OfferViewModelUtil.java similarity index 64% rename from desktop/src/main/java/bisq/desktop/main/offer/FeeUtil.java rename to desktop/src/main/java/bisq/desktop/main/offer/OfferViewModelUtil.java index ddd2e28be3..b9c1575f33 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/FeeUtil.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/OfferViewModelUtil.java @@ -23,18 +23,24 @@ import bisq.desktop.util.GUIUtil; import bisq.core.locale.Res; import bisq.core.monetary.Volume; import bisq.core.offer.OfferUtil; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.CoinFormatter; +import bisq.common.app.DevEnv; + import org.bitcoinj.core.Coin; import java.util.Optional; -public class FeeUtil { +// Shared utils for ViewModels +public class OfferViewModelUtil { public static String getTradeFeeWithFiatEquivalent(OfferUtil offerUtil, Coin tradeFee, CoinFormatter formatter) { - Optional optionalBtcFeeInFiat = offerUtil.getFeeInUserFiatCurrency(tradeFee); + Optional optionalBtcFeeInFiat = offerUtil.getFeeInUserFiatCurrency(tradeFee, + formatter); + return DisplayUtils.getFeeWithFiatAmount(tradeFee, optionalBtcFeeInFiat, formatter); } @@ -43,19 +49,20 @@ public class FeeUtil { Coin tradeAmount, CoinFormatter formatter, Coin minTradeFee) { - String feeAsBtc = formatter.formatCoinWithCode(tradeFee); - String percentage; - if (!tradeFee.isGreaterThan(minTradeFee)) { - percentage = Res.get("guiUtil.requiredMinimum") - .replace("(", "") - .replace(")", ""); - } else { - percentage = GUIUtil.getPercentage(tradeFee, tradeAmount) + - " " + Res.get("guiUtil.ofTradeAmount"); - } - return offerUtil.getFeeInUserFiatCurrency(tradeFee) - .map(DisplayUtils::formatAverageVolumeWithCode) - .map(feeInFiat -> Res.get("feeOptionWindow.btcFeeWithFiatAndPercentage", feeAsBtc, feeInFiat, percentage)) - .orElseGet(() -> Res.get("feeOptionWindow.btcFeeWithPercentage", feeAsBtc, percentage)); + String feeAsBtc = formatter.formatCoinWithCode(tradeFee); + String percentage; + if (!tradeFee.isGreaterThan(minTradeFee)) { + percentage = Res.get("guiUtil.requiredMinimum") + .replace("(", "") + .replace(")", ""); + } else { + percentage = GUIUtil.getPercentage(tradeFee, tradeAmount) + + " " + Res.get("guiUtil.ofTradeAmount"); + } + return offerUtil.getFeeInUserFiatCurrency(tradeFee, + formatter) + .map(VolumeUtil::formatAverageVolumeWithCode) + .map(feeInFiat -> Res.get("feeOptionWindow.btcFeeWithFiatAndPercentage", feeAsBtc, feeInFiat, percentage)) + .orElseGet(() -> Res.get("feeOptionWindow.btcFeeWithPercentage", feeAsBtc, percentage)); } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/OfferViewUtil.java b/desktop/src/main/java/bisq/desktop/main/offer/OfferViewUtil.java index 66bb1e5de6..5f9c96551f 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/OfferViewUtil.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/OfferViewUtil.java @@ -17,14 +17,48 @@ package bisq.desktop.main.offer; +import bisq.desktop.Navigation; +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.main.MainView; +import bisq.desktop.main.offer.offerbook.BtcOfferBookView; +import bisq.desktop.main.offer.offerbook.OfferBookView; +import bisq.desktop.main.offer.offerbook.OtherOfferBookView; +import bisq.desktop.main.offer.offerbook.TopAltcoinOfferBookView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.GUIUtil; + +import bisq.core.locale.CryptoCurrency; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; + +import bisq.common.UserThread; +import bisq.common.util.Tuple2; + import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.geometry.HPos; import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.VPos; -/** - * Reusable methods for CreateOfferView, TakeOfferView or other related views - */ +import java.util.HashMap; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.jetbrains.annotations.NotNull; + +// Shared utils for Views public class OfferViewUtil { + public static Label createPopOverLabel(String text) { final Label label = new Label(text); label.setPrefWidth(300); @@ -33,4 +67,103 @@ public class OfferViewUtil { label.setPadding(new Insets(10)); return label; } + + public static void showPaymentAccountWarning(String msgKey, + HashMap paymentAccountWarningDisplayed) { + if (msgKey == null || paymentAccountWarningDisplayed.getOrDefault(msgKey, false)) { + return; + } + paymentAccountWarningDisplayed.put(msgKey, true); + UserThread.runAfter(() -> { + new Popup().information(Res.get(msgKey)) + .width(900) + .closeButtonText(Res.get("shared.iConfirm")) + .dontShowAgainId(msgKey) + .show(); + }, 500, TimeUnit.MILLISECONDS); + } + + public static void addPayInfoEntry(GridPane infoGridPane, int row, String labelText, String value) { + Label label = new AutoTooltipLabel(labelText); + TextField textField = new TextField(value); + textField.setMinWidth(500); + textField.setEditable(false); + textField.setFocusTraversable(false); + textField.setId("payment-info"); + GridPane.setConstraints(label, 0, row, 1, 1, HPos.RIGHT, VPos.CENTER); + GridPane.setConstraints(textField, 1, row); + infoGridPane.getChildren().addAll(label, textField); + } + + public static Tuple2 createBuyBsqButtonBox(Navigation navigation) { + String buyBsqText = Res.get("shared.buyCurrency", "BSQ"); + var buyBsqButton = new AutoTooltipButton(buyBsqText); + buyBsqButton.getStyleClass().add("action-button"); + buyBsqButton.getStyleClass().add("tiny-button"); + buyBsqButton.setMinWidth(60); + buyBsqButton.setOnAction(e -> openBuyBsqOfferBook(navigation) + ); + + var info = new AutoTooltipLabel("BSQ is colored BTC that helps fund Bisq developers."); + var learnMore = new HyperlinkWithIcon("Learn More"); + learnMore.setOnAction(e -> new Popup().headLine(buyBsqText) + .information(Res.get("createOffer.buyBsq.popupMessage")) + .actionButtonText(buyBsqText) + .buttonAlignment(HPos.CENTER) + .onAction(() -> openBuyBsqOfferBook(navigation)).show()); + learnMore.setMinWidth(100); + + HBox buyBsqBox = new HBox(buyBsqButton, info, learnMore); + buyBsqBox.setAlignment(Pos.BOTTOM_LEFT); + buyBsqBox.setSpacing(10); + buyBsqBox.setPadding(new Insets(0, 0, 4, -20)); + + return new Tuple2<>(buyBsqButton, buyBsqBox); + } + + private static void openBuyBsqOfferBook(Navigation navigation) { + throw new Error("BSQ not supported"); + } + + public static Class> getOfferBookViewClass(String currencyCode) { + Class> offerBookViewClazz; + if (CurrencyUtil.isFiatCurrency(currencyCode)) { + offerBookViewClazz = BtcOfferBookView.class; + } else if (currencyCode.equals(GUIUtil.TOP_ALTCOIN.getCode())) { + offerBookViewClazz = TopAltcoinOfferBookView.class; + } else { + offerBookViewClazz = OtherOfferBookView.class; + } + return offerBookViewClazz; + } + + public static boolean isShownAsSellOffer(Offer offer) { + return isShownAsSellOffer(offer.getCurrencyCode(), offer.getDirection()); + } + + public static boolean isShownAsSellOffer(TradeCurrency tradeCurrency, OfferDirection direction) { + return isShownAsSellOffer(tradeCurrency.getCode(), direction); + } + + public static boolean isShownAsSellOffer(String currencyCode, OfferDirection direction) { + return CurrencyUtil.isFiatCurrency(currencyCode) == (direction == OfferDirection.SELL); + } + + public static boolean isShownAsBuyOffer(Offer offer) { + return !isShownAsSellOffer(offer); + } + + public static boolean isShownAsBuyOffer(OfferDirection direction, TradeCurrency tradeCurrency) { + return !isShownAsSellOffer(tradeCurrency.getCode(), direction); + } + + public static TradeCurrency getAnyOfMainCryptoCurrencies() { + return getMainCryptoCurrencies().findAny().get(); + } + + @NotNull + public static Stream getMainCryptoCurrencies() { + return CurrencyUtil.getMainCryptoCurrencies().stream().filter(cryptoCurrency -> + !Objects.equals(cryptoCurrency.getCode(), GUIUtil.TOP_ALTCOIN.getCode())); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/SelectableView.java b/desktop/src/main/java/bisq/desktop/main/offer/SelectableView.java new file mode 100644 index 0000000000..dda7cb7f8a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/offer/SelectableView.java @@ -0,0 +1,22 @@ +/* + * 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 . + */ + +package bisq.desktop.main.offer; + +public interface SelectableView { + public void onTabSelected(boolean isSelected); +} diff --git a/desktop/src/main/java/bisq/desktop/main/offer/SellOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/SellOfferView.java index 46285cc977..cc656250a4 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/SellOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/SellOfferView.java @@ -20,7 +20,7 @@ package bisq.desktop.main.offer; import bisq.desktop.Navigation; import bisq.desktop.common.view.FxmlView; import bisq.desktop.common.view.ViewLoader; - +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferPayload; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.user.Preferences; @@ -37,15 +37,13 @@ public class SellOfferView extends OfferView { public SellOfferView(ViewLoader viewLoader, Navigation navigation, Preferences preferences, - ArbitratorManager arbitratorManager, User user, P2PService p2PService) { super(viewLoader, navigation, preferences, - arbitratorManager, user, p2PService, - OfferPayload.Direction.SELL); + OfferDirection.SELL); } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java index 43eac5ebe4..fbe0f5b98b 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java @@ -1,34 +1,30 @@ /* - * This file is part of Bisq. + * This file is part of Haveno. * - * Bisq is free software: you can redistribute it and/or modify it + * 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 + * the Free Software Foundation, either version 3 of the License, or (at * your option) any later version. * - * Bisq is distributed in the hope that it will be useful, -but WITHOUT + * 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 Bisq. If not, -see . + * along with Haveno. If not, see . */ package bisq.desktop.main.offer.createoffer; import bisq.desktop.Navigation; import bisq.desktop.main.offer.MutableOfferDataModel; - import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.offer.CreateOfferService; import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOfferManager; +import bisq.core.payment.PaymentAccount; import bisq.core.provider.fee.FeeService; import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.statistics.TradeStatisticsManager; @@ -40,7 +36,9 @@ import bisq.core.util.coin.CoinFormatter; import bisq.network.p2p.P2PService; import com.google.inject.Inject; - +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Named; /** @@ -78,4 +76,10 @@ class CreateOfferDataModel extends MutableOfferDataModel { tradeStatisticsManager, navigation); } + + @Override + protected Set getUserPaymentAccounts() { + return Objects.requireNonNull(user.getPaymentAccounts()).stream() + .collect(Collectors.toSet()); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferView.java index 7b62043891..a36a727c85 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferView.java @@ -20,14 +20,22 @@ package bisq.desktop.main.offer.createoffer; import bisq.desktop.Navigation; import bisq.desktop.common.view.FxmlView; import bisq.desktop.main.offer.MutableOfferView; +import bisq.desktop.main.offer.OfferView; import bisq.desktop.main.overlays.windows.OfferDetailsWindow; - +import bisq.desktop.util.GUIUtil; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.TradeCurrency; +import bisq.core.offer.OfferDirection; +import bisq.core.payment.PaymentAccount; import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; import com.google.inject.Inject; - +import java.util.Objects; +import java.util.stream.Collectors; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javax.inject.Named; @FxmlView @@ -41,4 +49,29 @@ public class CreateOfferView extends MutableOfferView { @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { super(model, navigation, preferences, offerDetailsWindow, btcFormatter); } + + @Override + public void initWithData(OfferDirection direction, + TradeCurrency tradeCurrency, + OfferView.OfferActionHandler offerActionHandler) { + // Invert direction for non-Fiat trade currencies -> BUY BSQ is to SELL Bitcoin + OfferDirection offerDirection = CurrencyUtil.isFiatCurrency(tradeCurrency.getCode()) ? direction : + direction == OfferDirection.BUY ? OfferDirection.SELL : OfferDirection.BUY; + super.initWithData(offerDirection, tradeCurrency, offerActionHandler); + } + + @Override + protected ObservableList filterPaymentAccounts(ObservableList paymentAccounts) { + return FXCollections.observableArrayList( + paymentAccounts.stream().filter(paymentAccount -> { + if (model.getTradeCurrency().equals(GUIUtil.TOP_ALTCOIN)) { + return Objects.equals(paymentAccount.getSingleTradeCurrency(), GUIUtil.TOP_ALTCOIN); + } else if (CurrencyUtil.isFiatCurrency(model.getTradeCurrency().getCode())) { + return !paymentAccount.getPaymentMethod().isAltcoin(); + } else { + return paymentAccount.getPaymentMethod().isAltcoin() && + !Objects.equals(paymentAccount.getSingleTradeCurrency(), GUIUtil.TOP_ALTCOIN); + } + }).collect(Collectors.toList())); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/BtcOfferBookView.fxml b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/BtcOfferBookView.fxml new file mode 100644 index 0000000000..c51e87d63d --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/BtcOfferBookView.fxml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/BtcOfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/BtcOfferBookView.java new file mode 100644 index 0000000000..7013a233e0 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/BtcOfferBookView.java @@ -0,0 +1,67 @@ +/* + * 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 . + */ + +package bisq.desktop.main.offer.offerbook; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.overlays.windows.OfferDetailsWindow; + +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.locale.Res; +import bisq.core.offer.OfferDirection; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.CoinFormatter; + +import bisq.common.config.Config; + +import javax.inject.Inject; +import javax.inject.Named; + +import javafx.scene.layout.GridPane; + +@FxmlView +public class BtcOfferBookView extends OfferBookView { + + @Inject + BtcOfferBookView(BtcOfferBookViewModel model, + Navigation navigation, + OfferDetailsWindow offerDetailsWindow, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + PrivateNotificationManager privateNotificationManager, + @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys, + AccountAgeWitnessService accountAgeWitnessService, + SignedWitnessService signedWitnessService) { + super(model, navigation, offerDetailsWindow, formatter, privateNotificationManager, useDevPrivilegeKeys, accountAgeWitnessService, signedWitnessService); + } + + @Override + protected String getMarketTitle() { + return model.getDirection().equals(OfferDirection.BUY) ? + Res.get("offerbook.availableOffersToBuy", Res.getBaseCurrencyCode(), Res.get("shared.fiat")) : + Res.get("offerbook.availableOffersToSell", Res.getBaseCurrencyCode(), Res.get("shared.fiat")); + + + } + + @Override + String getTradeCurrencyCode() { + return Res.getBaseCurrencyCode(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/BtcOfferBookViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/BtcOfferBookViewModel.java new file mode 100644 index 0000000000..326987895c --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/BtcOfferBookViewModel.java @@ -0,0 +1,158 @@ +/* + * 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 . + */ + +package bisq.desktop.main.offer.offerbook; + +import bisq.desktop.Navigation; +import bisq.desktop.util.GUIUtil; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.api.CoreApi; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.locale.CryptoCurrency; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.GlobalSettings; +import bisq.core.locale.TradeCurrency; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; +import bisq.core.offer.OfferFilterService; +import bisq.core.offer.OpenOfferManager; +import bisq.core.payment.PaymentAccountUtil; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.ClosedTradableManager; +import bisq.core.user.Preferences; +import bisq.core.user.User; +import bisq.core.util.FormattingUtils; +import bisq.core.util.PriceUtil; +import bisq.core.util.coin.CoinFormatter; + +import bisq.network.p2p.P2PService; + +import com.google.inject.Inject; + +import javax.inject.Named; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class BtcOfferBookViewModel extends OfferBookViewModel { + + @Inject + public BtcOfferBookViewModel(User user, + OpenOfferManager openOfferManager, + OfferBook offerBook, + Preferences preferences, + WalletsSetup walletsSetup, + P2PService p2PService, + PriceFeedService priceFeedService, + ClosedTradableManager closedTradableManager, + AccountAgeWitnessService accountAgeWitnessService, + Navigation navigation, + PriceUtil priceUtil, + OfferFilterService offerFilterService, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + CoreApi coreApi) { + super(user, openOfferManager, offerBook, preferences, walletsSetup, p2PService, priceFeedService, closedTradableManager, accountAgeWitnessService, navigation, priceUtil, offerFilterService, btcFormatter, coreApi); + } + + @Override + void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code) { + if (direction == OfferDirection.BUY) { + preferences.setBuyScreenCurrencyCode(code); + } else { + preferences.setSellScreenCurrencyCode(code); + } + } + + @Override + protected ObservableList filterPaymentMethods(ObservableList list, + TradeCurrency selectedTradeCurrency) { + return FXCollections.observableArrayList(list.stream() + .filter(paymentMethod -> { + if (showAllTradeCurrenciesProperty.get()) { + return paymentMethod.isFiat(); + } + return paymentMethod.isFiat() && + PaymentAccountUtil.supportsCurrency(paymentMethod, selectedTradeCurrency); + }) + .collect(Collectors.toList())); + } + + @Override + void fillCurrencies(ObservableList tradeCurrencies, + ObservableList allCurrencies) { + // Used for ignoring filter (show all) + tradeCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); + tradeCurrencies.addAll(preferences.getFiatCurrenciesAsObservable()); + tradeCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); + + allCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); + allCurrencies.addAll(CurrencyUtil.getAllSortedFiatCurrencies()); + allCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); + } + + @Override + Predicate getCurrencyAndMethodPredicate(OfferDirection direction, + TradeCurrency selectedTradeCurrency) { + return offerBookListItem -> { + Offer offer = offerBookListItem.getOffer(); + boolean directionResult = offer.getDirection() != direction; + boolean currencyResult = (showAllTradeCurrenciesProperty.get() && offer.isFiatOffer()) || + offer.getCurrencyCode().equals(selectedTradeCurrency.getCode()); + boolean paymentMethodResult = showAllPaymentMethods || + offer.getPaymentMethod().equals(selectedPaymentMethod); + boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); + return directionResult && currencyResult && paymentMethodResult && notMyOfferOrShowMyOffersActivated; + }; + } + + @Override + TradeCurrency getDefaultTradeCurrency() { + TradeCurrency defaultTradeCurrency = GlobalSettings.getDefaultTradeCurrency(); + + if (CurrencyUtil.isFiatCurrency(defaultTradeCurrency.getCode()) && hasPaymentAccountForCurrency(defaultTradeCurrency)) { + return defaultTradeCurrency; + } + + ObservableList tradeCurrencies = FXCollections.observableArrayList(getTradeCurrencies()); + if (!tradeCurrencies.isEmpty()) { + // drop show all entry and select first currency with payment account available + tradeCurrencies.remove(0); + List sortedList = tradeCurrencies.stream().sorted((o1, o2) -> + Boolean.compare(!hasPaymentAccountForCurrency(o1), + !hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()); + return sortedList.get(0); + } else { + return CurrencyUtil.getMainFiatCurrencies().stream().sorted((o1, o2) -> + Boolean.compare(!hasPaymentAccountForCurrency(o1), + !hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()).get(0); + } + } + + @Override + String getCurrencyCodeFromPreferences(OfferDirection direction) { + // validate if previous stored currencies are Fiat ones + String currencyCode = direction == OfferDirection.BUY ? preferences.getBuyScreenCurrencyCode() : preferences.getSellScreenCurrencyCode(); + + return CurrencyUtil.isFiatCurrency(currencyCode) ? currencyCode : null; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java index fe71388c84..e9586cedad 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java @@ -20,7 +20,10 @@ package bisq.desktop.main.offer.offerbook; import bisq.core.filter.FilterManager; import bisq.core.offer.Offer; import bisq.core.offer.OfferBookService; -import bisq.core.trade.TradeManager; +import bisq.core.offer.OfferRestrictions; + +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.utils.Utils; import javax.inject.Inject; import javax.inject.Singleton; @@ -29,13 +32,14 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; 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; -import static bisq.core.offer.OfferPayload.Direction.BUY; +import static bisq.core.offer.OfferDirection.BUY; /** * Holds and manages the unsorted and unfiltered offerbook list (except for banned offers) of both buy and sell offers. @@ -53,18 +57,20 @@ public class OfferBook { private final Map sellOfferCountMap = new HashMap<>(); private final FilterManager filterManager; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject - OfferBook(OfferBookService offerBookService, TradeManager tradeManager, FilterManager filterManager) { + OfferBook(OfferBookService offerBookService, FilterManager filterManager) { this.offerBookService = offerBookService; this.filterManager = filterManager; offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() { @Override public void onAdded(Offer offer) { + printOfferBookListItems("Before onAdded"); // We get onAdded called every time a new ProtectedStorageEntry is received. // Mostly it is the same OfferPayload but the ProtectedStorageEntry is different. // We filter here to only add new offers if the same offer (using equals) was not already added and it @@ -75,44 +81,120 @@ public class OfferBook { return; } - boolean hasSameOffer = offerBookListItems.stream() - .anyMatch(item -> item.getOffer().equals(offer)); - if (!hasSameOffer) { - OfferBookListItem offerBookListItem = new OfferBookListItem(offer); - // We don't use the contains method as the equals method in Offer takes state and errorMessage into account. - // If we have an offer with same ID we remove it and add the new offer as it might have a changed state. - Optional candidateWithSameId = offerBookListItems.stream() - .filter(item -> item.getOffer().getId().equals(offer.getId())) - .findAny(); - if (candidateWithSameId.isPresent()) { - log.warn("We had an old offer in the list with the same Offer ID {}. We remove the old one. " + - "old offerBookListItem={}, new offerBookListItem={}", offer.getId(), candidateWithSameId.get(), offerBookListItem); - offerBookListItems.remove(candidateWithSameId.get()); - } + if (OfferRestrictions.requiresNodeAddressUpdate() && !Utils.isV3Address(offer.getMakerNodeAddress().getHostName())) { + log.debug("Ignored offer with Tor v2 node address. ID={}", offer.getId()); + return; + } - offerBookListItems.add(offerBookListItem); + // Use offer.equals(offer) to see if the OfferBook list contains an exact + // match -- offer.equals(offer) includes comparisons of payload, state + // and errorMessage. + boolean hasSameOffer = offerBookListItems.stream().anyMatch(item -> item.getOffer().equals(offer)); + if (!hasSameOffer) { + OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer); + removeDuplicateItem(newOfferBookListItem); + offerBookListItems.add(newOfferBookListItem); // Add replacement. + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. + log.debug("onAdded: Added new offer {}\n" + + "\twith newItem.payloadHash: {}", + offer.getId(), + newOfferBookListItem.hashOfPayload.getHex()); + } } else { log.debug("We have the exact same offer already in our list and ignore the onAdded call. ID={}", offer.getId()); } + printOfferBookListItems("After onAdded"); } @Override public void onRemoved(Offer offer) { - removeOffer(offer, tradeManager); + printOfferBookListItems("Before onRemoved"); + removeOffer(offer); + printOfferBookListItems("After onRemoved"); + } + }); + + filterManager.filterProperty().addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + // any notifications } }); } - public void removeOffer(Offer offer, TradeManager tradeManager) { + private void removeDuplicateItem(OfferBookListItem newOfferBookListItem) { + String offerId = newOfferBookListItem.getOffer().getId(); + // We need to remove any view items with a matching offerId before + // a newOfferBookListItem is added to the view. + List duplicateItems = offerBookListItems.stream() + .filter(item -> item.getOffer().getId().equals(offerId)) + .collect(Collectors.toList()); + duplicateItems.forEach(oldOfferItem -> { + offerBookListItems.remove(oldOfferItem); + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. + log.debug("onAdded: Removed old offer {}\n" + + "\twith payload hash {} from list.\n" + + "\tThis may make a subsequent onRemoved( {} ) call redundant.", + offerId, + oldOfferItem.getHashOfPayload().getHex(), + oldOfferItem.getOffer().getId()); + } + }); + } + + public void removeOffer(Offer offer) { // Update state in case that that offer is used in the take offer screen, so it gets updated correctly offer.setState(Offer.State.REMOVED); - offer.cancelAvailabilityRequest(); - // We don't use the contains method as the equals method in Offer takes state and errorMessage into account. - Optional candidateToRemove = offerBookListItems.stream() - .filter(item -> item.getOffer().getId().equals(offer.getId())) + + P2PDataStorage.ByteArray hashOfPayload = new P2PDataStorage.ByteArray(offer.getOfferPayload().getHash()); + + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. + log.debug("onRemoved: id = {}\n" + + "\twith payload-hash = {}", + offer.getId(), + hashOfPayload.getHex()); + } + + // Find the removal candidate in the OfferBook list with matching offerId and payload-hash. + Optional candidateWithMatchingPayloadHash = offerBookListItems.stream() + .filter(item -> item.getOffer().getId().equals(offer.getId()) + && item.hashOfPayload.equals(hashOfPayload)) .findAny(); - candidateToRemove.ifPresent(offerBookListItems::remove); + + if (!candidateWithMatchingPayloadHash.isPresent()) { + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. + log.debug("UI view list does not contain offer with id {} and payload-hash {}", + offer.getId(), + hashOfPayload.getHex()); + } + return; + } + + OfferBookListItem candidate = candidateWithMatchingPayloadHash.get(); + // Remove the candidate only if the candidate's offer payload the hash matches the + // onRemoved hashOfPayload parameter. We may receive add/remove messages out of + // order from the API's 'editoffer' method, and use the offer payload hash to + // ensure we do not remove an edited offer immediately after it was added. + if (candidate.getHashOfPayload().equals(hashOfPayload)) { + // The payload-hash test passed, remove the candidate and print reason. + offerBookListItems.remove(candidate); + + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. + log.debug("Candidate.payload-hash: {} == onRemoved.payload-hash: {} ?" + + " Yes, removed old offer", + candidate.hashOfPayload.getHex(), + hashOfPayload.getHex()); + } + } else { + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. + // Candidate's payload-hash test failed: payload-hash != onRemoved.payload-hash. + // Print reason for not removing candidate. + log.debug("Candidate.payload-hash: {} == onRemoved.payload-hash: {} ?" + + " No, old offer not removed", + candidate.hashOfPayload.getHex(), + hashOfPayload.getHex()); + } + } } public ObservableList getOfferBookListItems() { @@ -125,15 +207,28 @@ public class OfferBook { // Investigate why.... offerBookListItems.clear(); offerBookListItems.addAll(offerBookService.getOffers().stream() - .filter(o -> !filterManager.isOfferIdBanned(o.getId())) + .filter(this::isOfferAllowed) .map(OfferBookListItem::new) .collect(Collectors.toList())); log.debug("offerBookListItems.size {}", offerBookListItems.size()); fillOfferCountMaps(); } catch (Throwable t) { - t.printStackTrace(); - log.error("Error at fillOfferBookListItems: " + t.toString()); + log.error("Error at fillOfferBookListItems: " + t); + } + } + + public void printOfferBookListItems(String msg) { + if (log.isDebugEnabled()) { + if (offerBookListItems.size() == 0) { + log.debug("{} -> OfferBookListItems: none", msg); + return; + } + + StringBuilder stringBuilder = new StringBuilder(msg + " -> ").append("OfferBookListItems:").append("\n"); + offerBookListItems.forEach(i -> stringBuilder.append("\t").append(i.toString()).append("\n")); + stringBuilder.deleteCharAt(stringBuilder.length() - 1); + log.debug(stringBuilder.toString()); } } @@ -145,6 +240,13 @@ public class OfferBook { return sellOfferCountMap; } + private boolean isOfferAllowed(Offer offer) { + boolean isBanned = filterManager.isOfferIdBanned(offer.getId()); + boolean isV3NodeAddressCompliant = !OfferRestrictions.requiresNodeAddressUpdate() + || Utils.isV3Address(offer.getMakerNodeAddress().getHostName()); + return !isBanned && isV3NodeAddressCompliant; + } + private void fillOfferCountMaps() { buyOfferCountMap.clear(); sellOfferCountMap.clear(); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java index 29a350b990..0d04659751 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java @@ -27,6 +27,8 @@ import bisq.core.locale.Res; import bisq.core.offer.Offer; import bisq.core.payment.payload.PaymentMethod; +import bisq.network.p2p.storage.P2PDataStorage; + import de.jensd.fx.glyphs.GlyphIcons; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; @@ -38,119 +40,153 @@ import lombok.Getter; import lombok.Value; import lombok.extern.slf4j.Slf4j; -@Slf4j +import org.jetbrains.annotations.NotNull; +@Slf4j public class OfferBookListItem { @Getter private final Offer offer; + /** + * The protected storage (offer) payload hash helps prevent edited offers from being + * mistakenly removed from a UI user's OfferBook list if the API's 'editoffer' + * command results in onRemoved(offer) being called after onAdded(offer) on peers. + * (Checking the offer-id is not enough.) This msg order problem does not happen + * when the UI edits an offer because the remove/add msgs are always sent in separate + * envelope bundles. It can happen when the API is used to edit an offer because + * the remove/add msgs are received in the same envelope bundle, then processed in + * unpredictable order. + */ + @Getter + P2PDataStorage.ByteArray hashOfPayload; + // We cache the data once created for performance reasons. AccountAgeWitnessService calls can // be a bit expensive. private WitnessAgeData witnessAgeData; public OfferBookListItem(Offer offer) { this.offer = offer; + this.hashOfPayload = new P2PDataStorage.ByteArray(offer.getOfferPayload().getHash()); } public WitnessAgeData getWitnessAgeData(AccountAgeWitnessService accountAgeWitnessService, SignedWitnessService signedWitnessService) { if (witnessAgeData == null) { - long ageInMs; - long daysSinceSignedAsLong = -1; - long accountAgeDaysAsLong = -1; - long accountAgeDaysNotYetSignedAsLong = -1; - String displayString; - String info; - GlyphIcons icon; - if (CurrencyUtil.isCryptoCurrency(offer.getCurrencyCode())) { - // Altcoins - displayString = Res.get("offerbook.timeSinceSigning.notSigned.noNeed"); - info = Res.get("shared.notSigned.noNeedAlts"); - icon = MaterialDesignIcon.INFORMATION_OUTLINE; + witnessAgeData = new WitnessAgeData(WitnessAgeData.TYPE_ALTCOINS); } else if (PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode())) { // Fiat and signed witness required Optional optionalWitness = accountAgeWitnessService.findWitness(offer); - AccountAgeWitnessService.SignState signState = optionalWitness.map(accountAgeWitnessService::getSignState) + AccountAgeWitnessService.SignState signState = optionalWitness + .map(accountAgeWitnessService::getSignState) .orElse(AccountAgeWitnessService.SignState.UNSIGNED); - boolean isSignedAccountAgeWitness = optionalWitness.map(signedWitnessService::isSignedAccountAgeWitness) + + boolean isSignedAccountAgeWitness = optionalWitness + .map(signedWitnessService::isSignedAccountAgeWitness) .orElse(false); + if (isSignedAccountAgeWitness || !signState.equals(AccountAgeWitnessService.SignState.UNSIGNED)) { // either signed & limits lifted, or waiting for limits to be lifted // Or banned - daysSinceSignedAsLong = TimeUnit.MILLISECONDS.toDays(optionalWitness.map(witness -> - accountAgeWitnessService.getWitnessSignAge(witness, new Date())) - .orElse(0L)); - displayString = Res.get("offerbook.timeSinceSigning.daysSinceSigning", daysSinceSignedAsLong); - info = Res.get("offerbook.timeSinceSigning.info", signState.getDisplayString()); + witnessAgeData = new WitnessAgeData( + signState.isLimitLifted() ? WitnessAgeData.TYPE_SIGNED_AND_LIMIT_LIFTED : WitnessAgeData.TYPE_SIGNED_OR_BANNED, + optionalWitness.map(witness -> accountAgeWitnessService.getWitnessSignAge(witness, new Date())).orElse(0L), + signState); } else { - // Unsigned case - ageInMs = optionalWitness.map(e -> accountAgeWitnessService.getAccountAge(e, new Date())) - .orElse(-1L); - accountAgeDaysNotYetSignedAsLong = ageInMs > -1 ? TimeUnit.MILLISECONDS.toDays(ageInMs) : 0; - displayString = Res.get("offerbook.timeSinceSigning.notSigned"); - info = Res.get("shared.notSigned", accountAgeDaysNotYetSignedAsLong); + witnessAgeData = new WitnessAgeData( + WitnessAgeData.TYPE_NOT_SIGNED, + optionalWitness.map(e -> accountAgeWitnessService.getAccountAge(e, new Date())).orElse(0L), + signState + ); } - - icon = GUIUtil.getIconForSignState(signState); } else { // Fiat, no signed witness required, we show account age - ageInMs = accountAgeWitnessService.getAccountAge(offer); - accountAgeDaysAsLong = ageInMs > -1 ? TimeUnit.MILLISECONDS.toDays(ageInMs) : 0; - displayString = Res.get("offerbook.timeSinceSigning.notSigned.ageDays", accountAgeDaysAsLong); - info = Res.get("shared.notSigned.noNeedDays", accountAgeDaysAsLong); - icon = MaterialDesignIcon.CHECKBOX_MARKED_OUTLINE; + witnessAgeData = new WitnessAgeData( + WitnessAgeData.TYPE_NOT_SIGNING_REQUIRED, + accountAgeWitnessService.getAccountAge(offer) + ); } - - witnessAgeData = new WitnessAgeData(displayString, info, icon, daysSinceSignedAsLong, accountAgeDaysNotYetSignedAsLong, accountAgeDaysAsLong); } return witnessAgeData; } + @Override + public String toString() { + return "OfferBookListItem{" + + "offerId=" + offer.getId() + + ", hashOfPayload=" + hashOfPayload.getHex() + + ", witnessAgeData=" + (witnessAgeData == null ? "null" : witnessAgeData.displayString) + + '}'; + } + @Value - public static class WitnessAgeData { - private final String displayString; - private final String info; - private final GlyphIcons icon; - private final Long daysSinceSignedAsLong; - private final long accountAgeDaysNotYetSignedAsLong; - private final Long accountAgeDaysAsLong; + public static class WitnessAgeData implements Comparable { + String displayString; + String info; + GlyphIcons icon; // Used for sorting - private final Long type; + Long type; // Used for sorting - private final Long days; + Long days; - public WitnessAgeData(String displayString, - String info, - GlyphIcons icon, - long daysSinceSignedAsLong, - long accountAgeDaysNotYetSignedAsLong, - long accountAgeDaysAsLong) { - this.displayString = displayString; - this.info = info; - this.icon = icon; - this.daysSinceSignedAsLong = daysSinceSignedAsLong; - this.accountAgeDaysNotYetSignedAsLong = accountAgeDaysNotYetSignedAsLong; - this.accountAgeDaysAsLong = accountAgeDaysAsLong; + public static final long TYPE_SIGNED_AND_LIMIT_LIFTED = 4L; + public static final long TYPE_SIGNED_OR_BANNED = 3L; + public static final long TYPE_NOT_SIGNED = 2L; + public static final long TYPE_NOT_SIGNING_REQUIRED = 1L; + public static final long TYPE_ALTCOINS = 0L; - if (daysSinceSignedAsLong > -1) { - // First we show signed accounts sorted by days - this.type = 3L; - this.days = daysSinceSignedAsLong; - } else if (accountAgeDaysNotYetSignedAsLong > -1) { - // Next group is not yet signed accounts sorted by account age - this.type = 2L; - this.days = accountAgeDaysNotYetSignedAsLong; - } else if (accountAgeDaysAsLong > -1) { - // Next group is not signing required accounts sorted by account age - this.type = 1L; - this.days = accountAgeDaysAsLong; + public WitnessAgeData(long type) { + this(type, 0, null); + } + + public WitnessAgeData(long type, long days) { + this(type, days, null); + } + + public WitnessAgeData(long type, long age, AccountAgeWitnessService.SignState signState) { + this.type = type; + long days = age > -1 ? TimeUnit.MILLISECONDS.toDays(age) : 0; + this.days = days; + + if (type == TYPE_SIGNED_AND_LIMIT_LIFTED) { + this.displayString = Res.get("offerbook.timeSinceSigning.daysSinceSigning", days); + this.info = Res.get("offerbook.timeSinceSigning.tooltip.info.signedAndLifted"); + this.icon = GUIUtil.getIconForSignState(signState); + } else if (type == TYPE_SIGNED_OR_BANNED) { + this.displayString = Res.get("offerbook.timeSinceSigning.daysSinceSigning", days); + this.info = Res.get("offerbook.timeSinceSigning.tooltip.info.signed"); + this.icon = GUIUtil.getIconForSignState(signState); + } else if (type == TYPE_NOT_SIGNED) { + this.displayString = Res.get("offerbook.timeSinceSigning.notSigned"); + this.info = Res.get("offerbook.timeSinceSigning.tooltip.info.unsigned"); + this.icon = GUIUtil.getIconForSignState(signState); + } else if (type == TYPE_NOT_SIGNING_REQUIRED) { + this.displayString = Res.get("offerbook.timeSinceSigning.notSigned.ageDays", days); + this.info = Res.get("shared.notSigned.noNeedDays", days); + this.icon = MaterialDesignIcon.CHECKBOX_MARKED_OUTLINE; } else { - // No signing and age required (altcoins) - this.type = 0L; - this.days = 0L; + this.displayString = Res.get("offerbook.timeSinceSigning.notSigned.noNeed"); + this.info = Res.get("shared.notSigned.noNeedAlts"); + this.icon = MaterialDesignIcon.INFORMATION_OUTLINE; } } + + public boolean isAccountSigned() { + return this.type == TYPE_SIGNED_AND_LIMIT_LIFTED || this.type == TYPE_SIGNED_OR_BANNED; + } + + public boolean isLimitLifted() { + return this.type == TYPE_SIGNED_AND_LIMIT_LIFTED; + } + + public boolean isSigningRequired() { + return this.type != TYPE_NOT_SIGNING_REQUIRED && this.type != TYPE_ALTCOINS; + } + + @Override + public int compareTo(@NotNull WitnessAgeData o) { + return (int) (this.type.equals(o.getType()) ? this.days - o.getDays() : this.type - o.getType()); + } } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java index 387c6aedb6..e1d070c064 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java @@ -19,7 +19,7 @@ package bisq.desktop.main.offer.offerbook; import bisq.desktop.Navigation; import bisq.desktop.common.view.ActivatableViewAndModel; -import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AccountStatusTooltipLabel; import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.AutoTooltipSlideToggleButton; @@ -28,16 +28,20 @@ import bisq.desktop.components.AutocompleteComboBox; import bisq.desktop.components.ColoredDecimalPlacesWithZerosText; import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.InfoAutoTooltipLabel; -import bisq.desktop.components.PeerInfoIcon; +import bisq.desktop.components.PeerInfoIconTrading; import bisq.desktop.components.TitledGroupBg; import bisq.desktop.main.MainView; import bisq.desktop.main.account.AccountView; +import bisq.desktop.main.account.content.altcoinaccounts.AltCoinAccountsView; import bisq.desktop.main.account.content.fiataccounts.FiatAccountsView; import bisq.desktop.main.funds.FundsView; import bisq.desktop.main.funds.withdrawal.WithdrawalView; import bisq.desktop.main.offer.OfferView; +import bisq.desktop.main.offer.OfferViewUtil; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.OfferDetailsWindow; +import bisq.desktop.main.portfolio.PortfolioView; +import bisq.desktop.main.portfolio.editoffer.EditOfferView; import bisq.desktop.util.CssTheme; import bisq.desktop.util.FormBuilder; import bisq.desktop.util.GUIUtil; @@ -47,31 +51,26 @@ import bisq.core.account.sign.SignedWitnessService; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; import bisq.core.locale.CurrencyUtil; -import bisq.core.locale.FiatCurrency; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.monetary.Price; import bisq.core.offer.Offer; -import bisq.core.offer.OfferFilter; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferDirection; +import bisq.core.offer.OfferFilterService; import bisq.core.offer.OfferRestrictions; +import bisq.core.offer.OpenOffer; import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentMethod; import bisq.core.user.DontShowAgainLookup; -import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; import bisq.network.p2p.NodeAddress; import bisq.common.UserThread; import bisq.common.app.DevEnv; -import bisq.common.config.Config; import bisq.common.util.Tuple3; import org.bitcoinj.core.Coin; -import javax.inject.Inject; -import javax.inject.Named; - import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; @@ -89,6 +88,7 @@ import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.text.TextAlignment; @@ -111,14 +111,14 @@ import javafx.util.Callback; import javafx.util.StringConverter; import java.util.Comparator; +import java.util.Map; import java.util.Optional; import org.jetbrains.annotations.NotNull; import static bisq.desktop.util.FormBuilder.addTitledGroupBg; -@FxmlView -public class OfferBookView extends ActivatableViewAndModel { +abstract public class OfferBookView extends ActivatableViewAndModel { private final Navigation navigation; private final OfferDetailsWindow offerDetailsWindow; @@ -128,33 +128,39 @@ public class OfferBookView extends ActivatableViewAndModel currencyComboBox; + private TitledGroupBg titledGroupBg; + protected AutocompleteComboBox currencyComboBox; private AutocompleteComboBox paymentMethodComboBox; private AutoTooltipButton createOfferButton; private AutoTooltipSlideToggleButton matchingOffersToggle; - private AutoTooltipTableColumn amountColumn, volumeColumn, marketColumn, - priceColumn, paymentMethodColumn, depositColumn, signingStateColumn, avatarColumn; + private AutoTooltipTableColumn amountColumn; + private AutoTooltipTableColumn volumeColumn; + private AutoTooltipTableColumn marketColumn; + private AutoTooltipTableColumn priceColumn; + private AutoTooltipTableColumn depositColumn; + private AutoTooltipTableColumn signingStateColumn; + private AutoTooltipTableColumn avatarColumn; private TableView tableView; - private OfferView.OfferActionHandler offerActionHandler; private int gridRow = 0; private Label nrOfOffersLabel; private ListChangeListener offerListListener; private ChangeListener priceFeedUpdateCounterListener; private Subscription currencySelectionSubscriber; private static final int SHOW_ALL = 0; + private Label disabledCreateOfferButtonTooltip; + protected VBox currencyComboBoxContainer; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// - @Inject - OfferBookView(OfferBookViewModel model, + OfferBookView(M model, Navigation navigation, OfferDetailsWindow offerDetailsWindow, - @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + CoinFormatter formatter, PrivateNotificationManager privateNotificationManager, - @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys, + boolean useDevPrivilegeKeys, AccountAgeWitnessService accountAgeWitnessService, SignedWitnessService signedWitnessService) { super(model); @@ -172,7 +178,12 @@ public class OfferBookView extends ActivatableViewAndModel> currencyBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( Res.get("offerbook.filterByCurrency")); + currencyComboBoxContainer = currencyBoxTuple.first; currencyComboBox = currencyBoxTuple.third; currencyComboBox.setPrefWidth(270); @@ -195,13 +207,23 @@ public class OfferBookView extends ActivatableViewAndModel paymentMethodColumn = getPaymentMethodColumn(); tableView.getColumns().add(paymentMethodColumn); depositColumn = getDepositColumn(); tableView.getColumns().add(depositColumn); @@ -264,7 +286,7 @@ public class OfferBookView extends ActivatableViewAndModel Res.get(o.getOffer().getPaymentMethod().getId()))); avatarColumn.setComparator(Comparator.comparing(o -> model.getNumTrades(o.getOffer()))); depositColumn.setComparator(Comparator.comparing(item -> { - boolean isSellOffer = item.getOffer().getDirection() == OfferPayload.Direction.SELL; + boolean isSellOffer = item.getOffer().getDirection() == OfferDirection.SELL; Coin deposit = isSellOffer ? item.getOffer().getBuyerSecurityDeposit() : item.getOffer().getSellerSecurityDeposit(); @@ -290,10 +312,7 @@ public class OfferBookView extends ActivatableViewAndModel comparator = Comparator.comparing(e -> e.getWitnessAgeData(accountAgeWitnessService, signedWitnessService).getType(), Comparator.nullsFirst(Comparator.naturalOrder())); - signingStateColumn.setComparator(comparator. - thenComparing(e -> e.getWitnessAgeData(accountAgeWitnessService, signedWitnessService).getDays(), - Comparator.nullsFirst(Comparator.naturalOrder()))); + signingStateColumn.setComparator(Comparator.comparing(e -> e.getWitnessAgeData(accountAgeWitnessService, signedWitnessService), Comparator.nullsFirst(Comparator.naturalOrder()))); nrOfOffersLabel = new AutoTooltipLabel(""); nrOfOffersLabel.setId("num-offers"); @@ -312,22 +331,35 @@ public class OfferBookView extends ActivatableViewAndModel tableView.sort(); } + abstract protected String getMarketTitle(); + @Override protected void activate() { + titledGroupBg.setText(getMarketTitle()); + titledGroupBg.setHelpUrl(model.getDirection() == OfferDirection.SELL + ? "https://bisq.wiki/Introduction#In_a_nutshell" + : "https://bisq.wiki/Taking_an_offer"); + + Map offerCounts = OfferViewUtil.isShownAsBuyOffer(model.getDirection(), model.getSelectedTradeCurrency()) ? model.getSellOfferCounts() : model.getBuyOfferCounts(); currencyComboBox.setCellFactory(GUIUtil.getTradeCurrencyCellFactory(Res.get("shared.oneOffer"), Res.get("shared.multipleOffers"), - (model.getDirection() == OfferPayload.Direction.BUY ? model.getSellOfferCounts() : model.getBuyOfferCounts()))); + offerCounts)); currencyComboBox.setConverter(new CurrencyStringConverter(currencyComboBox)); currencyComboBox.getEditor().getStyleClass().add("combo-box-editor-bold"); - currencyComboBox.setAutocompleteItems(model.getTradeCurrencies()); + currencyComboBox.setAutocompleteItems(model.getTradeCurrencies(), model.getAllCurrencies()); currencyComboBox.setVisibleRowCount(Math.min(currencyComboBox.getItems().size(), 10)); currencyComboBox.setOnChangeConfirmed(e -> { if (currencyComboBox.getEditor().getText().isEmpty()) currencyComboBox.getSelectionModel().select(SHOW_ALL); model.onSetTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem()); + paymentMethodComboBox.setAutocompleteItems(model.getPaymentMethods()); + model.updateSelectedPaymentMethod(); + updatePaymentMethodComboBoxEditor(); + model.onSetPaymentMethod(paymentMethodComboBox.getSelectionModel().getSelectedItem()); + updateCreateOfferButton(); }); updateCurrencyComboBoxFromModel(); @@ -369,18 +401,13 @@ public class OfferBookView extends ActivatableViewAndModel onCreateOffer()); MonadicBinding currencySelectionBinding = EasyBind.combine( model.showAllTradeCurrenciesProperty, model.tradeCurrencyCode, (showAll, code) -> { - setDirectionTitles(); if (showAll) { volumeColumn.setTitleWithHelpText(Res.get("shared.amountMinMax"), Res.get("shared.amountHelp")); priceColumn.setTitle(Res.get("shared.price")); @@ -412,7 +439,15 @@ public class OfferBookView extends ActivatableViewAndModel { - createOfferButton.setDisable(true); - offerActionHandler.onCreateOffer(model.getSelectedTradeCurrency()); - }) - .secondaryActionButtonText(Res.get("offerbook.setupNewAccount")) - .onSecondaryAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); - navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + if (CurrencyUtil.isFiatCurrency(model.getSelectedTradeCurrency().getCode())) { + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } else { + navigation.navigateTo(MainView.class, AccountView.class, AltCoinAccountsView.class); + } }) .width(725) .show(); return; } - createOfferButton.setDisable(true); - offerActionHandler.onCreateOffer(model.getSelectedTradeCurrency()); + disableCreateOfferButton(); } } - private void onShowInfo(Offer offer, OfferFilter.Result result) { + private void onShowInfo(Offer offer, OfferFilterService.Result result) { switch (result) { - case VALID: - break; case API_DISABLED: DevEnv.logErrorAndThrowIfDevMode("We are in desktop and in the taker position " + "viewing offers, so it cannot be that we got that result as we are not an API user."); break; case HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER: - openPopupForMissingAccountSetup(Res.get("offerbook.warning.noMatchingAccount.headline"), - Res.get("offerbook.warning.noMatchingAccount.msg"), - FiatAccountsView.class, - "navigation.account"); + openPopupForMissingAccountSetup(offer); break; case HAS_NOT_SAME_PROTOCOL_VERSION: new Popup().warning(Res.get("offerbook.warning.wrongTradeProtocol")).show(); @@ -668,9 +685,10 @@ public class OfferBookView extends ActivatableViewAndModel offerActionHandler.onTakeOffer(offer)) + .onAction(() -> model.onTakeOffer(offer)) .show(); } else { - offerActionHandler.onTakeOffer(offer); + model.onTakeOffer(offer); } } } @@ -694,7 +712,10 @@ public class OfferBookView extends ActivatableViewAndModel doRemoveOffer(offer)) .closeButtonText(Res.get("shared.dontRemoveOffer")) @@ -706,6 +727,13 @@ public class OfferBookView extends ActivatableViewAndModel { navigation.setReturnPath(navigation.getCurrentPath()); - navigation.navigateTo(MainView.class, AccountView.class, target); + navigation.navigateTo(MainView.class, AccountView.class, accountViewClass); }).show(); } @@ -827,13 +859,14 @@ public class OfferBookView extends ActivatableViewAndModel 0) { - if (offer.isBuyOffer()) { + if (isShownAsBuyOffer) { info = Res.get("offerbook.info.sellBelowMarketPrice", absolutePriceMargin); } else { info = Res.get("offerbook.info.buyAboveMarketPrice", absolutePriceMargin); } } else { - if (offer.isBuyOffer()) { + if (isShownAsBuyOffer) { info = Res.get("offerbook.info.sellAboveMarketPrice", absolutePriceMargin); } else { info = Res.get("offerbook.info.buyBelowMarketPrice", absolutePriceMargin); @@ -855,7 +888,7 @@ public class OfferBookView extends ActivatableViewAndModel offerDetailsWindow.show(item.getOffer())); + field.setOnAction(event -> { + offerDetailsWindow.show(offer); + }); field.setTooltip(new Tooltip(model.getPaymentMethodToolTip(item))); setGraphic(field); } @@ -986,7 +1022,7 @@ public class OfferBookView extends ActivatableViewAndModel call(TableColumn column) { return new TableCell<>() { + OfferFilterService.Result canTakeOfferResult = null; + final ImageView iconView = new ImageView(); final AutoTooltipButton button = new AutoTooltipButton(); - OfferFilter.Result canTakeOfferResult = null; { button.setGraphic(iconView); - button.setMinWidth(200); - button.setMaxWidth(200); button.setGraphicTextGap(10); + button.setPrefWidth(10000); + } + + final ImageView iconView2 = new ImageView(); + final AutoTooltipButton button2 = new AutoTooltipButton(); + + { + button2.setGraphic(iconView2); + button2.setGraphicTextGap(10); + button2.setPrefWidth(10000); + } + + final HBox hbox = new HBox(); + + { + hbox.setSpacing(8); + hbox.setAlignment(Pos.CENTER); + hbox.getChildren().add(button); + hbox.getChildren().add(button2); + HBox.setHgrow(button, Priority.ALWAYS); + HBox.setHgrow(button2, Priority.ALWAYS); } @Override @@ -1044,11 +1100,15 @@ public class OfferBookView extends ActivatableViewAndModel onRemoveOpenOffer(offer)); + + iconView2.setId("image-edit"); + button2.updateText(Res.get("shared.edit")); + button2.setId(null); + button2.setStyle(CssTheme.isDarkTheme() ? "-fx-text-fill: white" : "-fx-text-fill: #444444"); + button2.setOnAction(e -> onEditOpenOffer(offer)); + button2.setManaged(true); + button2.setVisible(true); } else { - boolean isSellOffer = offer.getDirection() == OfferPayload.Direction.SELL; + boolean isSellOffer = OfferViewUtil.isShownAsSellOffer(offer); iconView.setId(isSellOffer ? "image-buy-white" : "image-sell-white"); button.setId(isSellOffer ? "buy-button" : "sell-button"); button.setStyle("-fx-text-fill: white"); - if (isSellOffer) { - title = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) ? - Res.get("offerbook.takeOfferToBuy", offer.getOfferPayload().getBaseCurrencyCode()) : - Res.get("offerbook.takeOfferToSell", offer.getCurrencyCode()); - } else { - title = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) ? - Res.get("offerbook.takeOfferToSell", offer.getOfferPayload().getBaseCurrencyCode()) : - Res.get("offerbook.takeOfferToBuy", offer.getCurrencyCode()); - } + title = Res.get("offerbook.takeOffer"); button.setTooltip(new Tooltip(Res.get("offerbook.takeOfferButton.tooltip", model.getDirectionLabelTooltip(offer)))); button.setOnAction(e -> onTakeOffer(offer)); + button2.setManaged(false); + button2.setVisible(false); } if (!myOffer) { if (canTakeOfferResult == null) { - canTakeOfferResult = model.offerFilter.canTakeOffer(offer, false); + canTakeOfferResult = model.offerFilterService.canTakeOffer(offer, false); } if (!canTakeOfferResult.isValid()) { @@ -1099,10 +1161,11 @@ public class OfferBookView extends ActivatableViewAndModel filteredItems; + private final CoreApi coreApi; private final SortedList sortedItems; private final ListChangeListener tradeCurrencyListChangeListener; private final ListChangeListener filterItemsListener; private TradeCurrency selectedTradeCurrency; - private final ObservableList allTradeCurrencies = FXCollections.observableArrayList(); + @Getter + private final ObservableList tradeCurrencies = FXCollections.observableArrayList(); + @Getter + private final ObservableList allCurrencies = FXCollections.observableArrayList(); - private OfferPayload.Direction direction; + private OfferDirection direction; final StringProperty tradeCurrencyCode = new SimpleStringProperty(); + private OfferView.OfferActionHandler offerActionHandler; + // If id is empty string we ignore filter (display all methods) PaymentMethod selectedPaymentMethod = getShowAllEntryForPaymentMethod(); - private boolean isTabSelected; + boolean isTabSelected; final BooleanProperty showAllTradeCurrenciesProperty = new SimpleBooleanProperty(true); final BooleanProperty disableMatchToggle = new SimpleBooleanProperty(); final IntegerProperty maxPlacesForAmount = new SimpleIntegerProperty(); @@ -137,40 +144,41 @@ class OfferBookViewModel extends ActivatableViewModel { // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// - @Inject public OfferBookViewModel(User user, OpenOfferManager openOfferManager, OfferBook offerBook, Preferences preferences, - CoreMoneroConnectionsService connectionService, + WalletsSetup walletsSetup, P2PService p2PService, PriceFeedService priceFeedService, ClosedTradableManager closedTradableManager, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, PriceUtil priceUtil, - OfferFilter offerFilter, - @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { + OfferFilterService offerFilterService, + CoinFormatter btcFormatter, + CoreApi coreApi) { super(); this.openOfferManager = openOfferManager; this.user = user; this.offerBook = offerBook; this.preferences = preferences; - this.connectionService = connectionService; + this.walletsSetup = walletsSetup; this.p2PService = p2PService; this.priceFeedService = priceFeedService; this.closedTradableManager = closedTradableManager; this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.priceUtil = priceUtil; - this.offerFilter = offerFilter; + this.offerFilterService = offerFilterService; this.btcFormatter = btcFormatter; this.filteredItems = new FilteredList<>(offerBook.getOfferBookListItems()); + this.coreApi = coreApi; this.sortedItems = new SortedList<>(filteredItems); - tradeCurrencyListChangeListener = c -> fillAllTradeCurrencies(); + tradeCurrencyListChangeListener = c -> fillCurrencies(); filterItemsListener = c -> { final Optional highestAmountOffer = filteredItems.stream() @@ -200,9 +208,9 @@ class OfferBookViewModel extends ActivatableViewModel { final Optional highestMarketPriceMarginOffer = filteredItems.stream() .filter(o -> o.getOffer().isUseMarketBasedPrice()) - .max(Comparator.comparing(o -> new DecimalFormat("#0.00").format(o.getOffer().getMarketPriceMargin() * 100).length())); + .max(Comparator.comparing(o -> new DecimalFormat("#0.00").format(o.getOffer().getMarketPriceMarginPct() * 100).length())); - highestMarketPriceMarginOffer.ifPresent(offerBookListItem -> maxPlacesForMarketPriceMargin.set(formatMarketPriceMargin(offerBookListItem.getOffer(), false).length())); + highestMarketPriceMarginOffer.ifPresent(offerBookListItem -> maxPlacesForMarketPriceMargin.set(formatMarketPriceMarginPct(offerBookListItem.getOffer()).length())); }; } @@ -210,28 +218,17 @@ class OfferBookViewModel extends ActivatableViewModel { protected void activate() { filteredItems.addListener(filterItemsListener); - String code = direction == OfferPayload.Direction.BUY ? preferences.getBuyScreenCurrencyCode() : preferences.getSellScreenCurrencyCode(); - if (code != null && !code.isEmpty() && !isShowAllEntry(code) && - CurrencyUtil.getTradeCurrency(code).isPresent()) { - showAllTradeCurrenciesProperty.set(false); - selectedTradeCurrency = CurrencyUtil.getTradeCurrency(code).get(); - } else { - showAllTradeCurrenciesProperty.set(true); - selectedTradeCurrency = GlobalSettings.getDefaultTradeCurrency(); - } - tradeCurrencyCode.set(selectedTradeCurrency.getCode()); - if (user != null) { disableMatchToggle.set(user.getPaymentAccounts() == null || user.getPaymentAccounts().isEmpty()); } useOffersMatchingMyAccountsFilter = !disableMatchToggle.get() && isShowOffersMatchingMyAccounts(); - fillAllTradeCurrencies(); + fillCurrencies(); + updateSelectedTradeCurrency(); preferences.getTradeCurrenciesAsObservable().addListener(tradeCurrencyListChangeListener); offerBook.fillOfferBookListItems(); filterOffers(); setMarketPriceFeedCurrency(); - } @Override @@ -245,13 +242,18 @@ class OfferBookViewModel extends ActivatableViewModel { // API /////////////////////////////////////////////////////////////////////////////////////////// - void initWithDirection(OfferPayload.Direction direction) { + void initWithDirection(OfferDirection direction) { this.direction = direction; } void onTabSelected(boolean isSelected) { this.isTabSelected = isSelected; setMarketPriceFeedCurrency(); + + if (isTabSelected) { + updateSelectedTradeCurrency(); + filterOffers(); + } } /////////////////////////////////////////////////////////////////////////////////////////// @@ -273,14 +275,13 @@ class OfferBookViewModel extends ActivatableViewModel { setMarketPriceFeedCurrency(); filterOffers(); - if (direction == OfferPayload.Direction.BUY) - preferences.setBuyScreenCurrencyCode(code); - else - preferences.setSellScreenCurrencyCode(code); + saveSelectedCurrencyCodeInPreferences(direction, code); } } - void onSetPaymentMethod(PaymentMethod paymentMethod) { + abstract void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code); + + protected void onSetPaymentMethod(PaymentMethod paymentMethod) { if (paymentMethod == null) return; @@ -291,7 +292,7 @@ class OfferBookViewModel extends ActivatableViewModel { // If we select TransferWise we switch to show all currencies as TransferWise supports // sending to most currencies. if (paymentMethod.getId().equals(PaymentMethod.TRANSFERWISE_ID)) { - onSetTradeCurrency(getShowAllEntryForCurrency()); + onSetTradeCurrency(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); } } else { this.selectedPaymentMethod = getShowAllEntryForPaymentMethod(); @@ -335,14 +336,10 @@ class OfferBookViewModel extends ActivatableViewModel { return openOfferManager.isMyOffer(offer); } - OfferPayload.Direction getDirection() { + OfferDirection getDirection() { return direction; } - public ObservableList getTradeCurrencies() { - return allTradeCurrencies; - } - boolean isBootstrappedOrShowPopup() { return GUIUtil.isBootstrappedOrShowPopup(p2PService); } @@ -361,11 +358,16 @@ class OfferBookViewModel extends ActivatableViewModel { } } + list = filterPaymentMethods(list, selectedTradeCurrency); + list.sort(Comparator.naturalOrder()); list.add(0, getShowAllEntryForPaymentMethod()); return list; } + protected abstract ObservableList filterPaymentMethods(ObservableList list, + TradeCurrency selectedTradeCurrency); + String getAmount(OfferBookListItem item) { return formatAmount(item.getOffer(), true); } @@ -390,7 +392,7 @@ class OfferBookViewModel extends ActivatableViewModel { } String getAbsolutePriceMargin(Offer offer) { - return FormattingUtils.formatPercentagePrice(Math.abs(offer.getMarketPriceMargin())); + return FormattingUtils.formatPercentagePrice(Math.abs(offer.getMarketPriceMarginPct())); } private String formatPrice(Offer offer, boolean decimalAligned) { @@ -404,17 +406,15 @@ class OfferBookViewModel extends ActivatableViewModel { } public Optional getMarketBasedPrice(Offer offer) { - return priceUtil.getMarketBasedPrice(offer, direction); + OfferDirection displayDirection = offer.isFiatOffer() ? direction : + direction.equals(OfferDirection.BUY) ? OfferDirection.SELL : OfferDirection.BUY; + return priceUtil.getMarketBasedPrice(offer, displayDirection); } - String formatMarketPriceMargin(Offer offer, boolean decimalAligned) { + String formatMarketPriceMarginPct(Offer offer) { String postFix = ""; if (offer.isUseMarketBasedPrice()) { - postFix = " (" + FormattingUtils.formatPercentagePrice(offer.getMarketPriceMargin()) + ")"; - } - - if (decimalAligned) { - postFix = FormattingUtils.fillUpPlacesWithEmptyStrings(postFix, maxPlacesForMarketPriceMargin.get()); + postFix = " (" + FormattingUtils.formatPercentagePrice(offer.getMarketPriceMarginPct()) + ")"; } return postFix; @@ -430,14 +430,14 @@ class OfferBookViewModel extends ActivatableViewModel { if (offerVolume != null && minOfferVolume != null) { String postFix = showAllTradeCurrenciesProperty.get() ? " " + offer.getCurrencyCode() : ""; decimalAligned = decimalAligned && !showAllTradeCurrenciesProperty.get(); - return DisplayUtils.formatVolume(offer, decimalAligned, maxPlacesForVolume.get()) + postFix; + return VolumeUtil.formatVolume(offer, decimalAligned, maxPlacesForVolume.get()) + postFix; } else { return Res.get("shared.na"); } } int getNumberOfDecimalsForVolume(OfferBookListItem item) { - return CurrencyUtil.isFiatCurrency(item.getOffer().getCurrencyCode()) ? GUIUtil.FIAT_DECIMALS_WITH_ZEROS : GUIUtil.ALTCOINS_DECIMALS_WITH_ZEROS; + return item.getOffer().isFiatOffer() ? GUIUtil.FIAT_DECIMALS_WITH_ZEROS : GUIUtil.ALTCOINS_DECIMALS_WITH_ZEROS; } String getPaymentMethod(OfferBookListItem item) { @@ -533,35 +533,36 @@ class OfferBookViewModel extends ActivatableViewModel { private void setMarketPriceFeedCurrency() { if (isTabSelected) { if (showAllTradeCurrenciesProperty.get()) - priceFeedService.setCurrencyCode(GlobalSettings.getDefaultTradeCurrency().getCode()); + priceFeedService.setCurrencyCode(getDefaultTradeCurrency().getCode()); else priceFeedService.setCurrencyCode(tradeCurrencyCode.get()); } } - private void fillAllTradeCurrencies() { - allTradeCurrencies.clear(); - // Used for ignoring filter (show all) - allTradeCurrencies.add(getShowAllEntryForCurrency()); - allTradeCurrencies.addAll(preferences.getTradeCurrenciesAsObservable()); - allTradeCurrencies.add(getEditEntryForCurrency()); + private void fillCurrencies() { + tradeCurrencies.clear(); + allCurrencies.clear(); + + fillCurrencies(tradeCurrencies, allCurrencies); } + abstract void fillCurrencies(ObservableList tradeCurrencies, + ObservableList allCurrencies); /////////////////////////////////////////////////////////////////////////////////////////// // Checks /////////////////////////////////////////////////////////////////////////////////////////// boolean hasPaymentAccountForCurrency() { - return (showAllTradeCurrenciesProperty.get() && - user.getPaymentAccounts() != null && - !user.getPaymentAccounts().isEmpty()) || - user.hasPaymentAccountForCurrency(selectedTradeCurrency); + return hasPaymentAccountForCurrency(selectedTradeCurrency); + } + + boolean hasPaymentAccountForCurrency(TradeCurrency currency) { + return user.hasPaymentAccountForCurrency(currency); } boolean canCreateOrTakeOffer() { return GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation) && - GUIUtil.isChainHeightSyncedWithinToleranceOrShowPopup(connectionService) && GUIUtil.isBootstrappedOrShowPopup(p2PService); } @@ -572,33 +573,23 @@ class OfferBookViewModel extends ActivatableViewModel { private void filterOffers() { Predicate predicate = useOffersMatchingMyAccountsFilter ? - getCurrencyAndMethodPredicate().and(getOffersMatchingMyAccountsPredicate()) : - getCurrencyAndMethodPredicate(); + getCurrencyAndMethodPredicate(direction, selectedTradeCurrency).and(getOffersMatchingMyAccountsPredicate()) : + getCurrencyAndMethodPredicate(direction, selectedTradeCurrency); filteredItems.setPredicate(predicate); } - private Predicate getCurrencyAndMethodPredicate() { - return offerBookListItem -> { - Offer offer = offerBookListItem.getOffer(); - boolean directionResult = offer.getDirection() != direction; - boolean currencyResult = (showAllTradeCurrenciesProperty.get()) || - offer.getCurrencyCode().equals(selectedTradeCurrency.getCode()); - boolean paymentMethodResult = showAllPaymentMethods || - offer.getPaymentMethod().equals(selectedPaymentMethod); - boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); - return directionResult && currencyResult && paymentMethodResult && notMyOfferOrShowMyOffersActivated; - }; - } + abstract Predicate getCurrencyAndMethodPredicate(OfferDirection direction, + TradeCurrency selectedTradeCurrency); private Predicate getOffersMatchingMyAccountsPredicate() { // This code duplicates code in the view at the button column. We need there the different results for // display in popups so we cannot replace that with the predicate. Any change need to be applied in both // places. - return offerBookListItem -> offerFilter.canTakeOffer(offerBookListItem.getOffer(), false).isValid(); + return offerBookListItem -> offerFilterService.canTakeOffer(offerBookListItem.getOffer(), false).isValid(); } boolean isOfferBanned(Offer offer) { - return offerFilter.isOfferBanned(offer); + return offerFilterService.isOfferBanned(offer); } private boolean isShowAllEntry(String id) { @@ -609,12 +600,13 @@ class OfferBookViewModel extends ActivatableViewModel { return id.equals(GUIUtil.EDIT_FLAG); } - int getNumTrades(Offer offer) { - return closedTradableManager.getObservableList().stream() + public int getNumTrades(Offer offer) { + return closedTradableManager.getTradableList().stream() + .filter(e -> e instanceof Trade) // weed out canceled offers .filter(e -> { - final NodeAddress tradingPeerNodeAddress = e instanceof Trade ? ((Trade) e).getTradingPeerNodeAddress() : null; - return tradingPeerNodeAddress != null && - tradingPeerNodeAddress.getFullAddress().equals(offer.getMakerNodeAddress().getFullAddress()); + final Optional tradingPeerNodeAddress = e.getOptionalTradingPeerNodeAddress(); + return tradingPeerNodeAddress.isPresent() && + tradingPeerNodeAddress.get().getFullAddress().equals(offer.getMakerNodeAddress().getFullAddress()); }) .collect(Collectors.toSet()) .size(); @@ -639,11 +631,11 @@ class OfferBookViewModel extends ActivatableViewModel { return btcFormatter.formatCoinWithCode(offer.getMakerFee()); } - private static String getDirectionWithCodeDetailed(OfferPayload.Direction direction, String currencyCode) { + private static String getDirectionWithCodeDetailed(OfferDirection direction, String currencyCode) { if (CurrencyUtil.isFiatCurrency(currencyCode)) - return (direction == OfferPayload.Direction.BUY) ? Res.get("shared.buyingBTCWith", currencyCode) : Res.get("shared.sellingBTCFor", currencyCode); + return (direction == OfferDirection.BUY) ? Res.get("shared.buyingBTCWith", currencyCode) : Res.get("shared.sellingBTCFor", currencyCode); else - return (direction == OfferPayload.Direction.SELL) ? Res.get("shared.buyingCurrency", currencyCode) : Res.get("shared.sellingCurrency", currencyCode); + return (direction == OfferDirection.SELL) ? Res.get("shared.buyingCurrency", currencyCode) : Res.get("shared.sellingCurrency", currencyCode); } public String formatDepositString(Coin deposit, long amount) { @@ -651,15 +643,49 @@ class OfferBookViewModel extends ActivatableViewModel { return btcFormatter.formatCoin(deposit) + " (" + percentage + ")"; } - private TradeCurrency getShowAllEntryForCurrency() { - return new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, ""); - } - - private TradeCurrency getEditEntryForCurrency() { - return new CryptoCurrency(GUIUtil.EDIT_FLAG, ""); - } - - private PaymentMethod getShowAllEntryForPaymentMethod() { + PaymentMethod getShowAllEntryForPaymentMethod() { return PaymentMethod.getDummyPaymentMethod(GUIUtil.SHOW_ALL_FLAG); } + + public boolean isInstantPaymentMethod(Offer offer) { + return offer.getPaymentMethod().equals(PaymentMethod.BLOCK_CHAINS_INSTANT); + } + + public void setOfferActionHandler(OfferView.OfferActionHandler offerActionHandler) { + this.offerActionHandler = offerActionHandler; + } + + public void onCreateOffer() { + offerActionHandler.onCreateOffer(getSelectedTradeCurrency(), selectedPaymentMethod); + } + + public void onTakeOffer(Offer offer) { + offerActionHandler.onTakeOffer(offer); + } + + private void updateSelectedTradeCurrency() { + String code = getCurrencyCodeFromPreferences(direction); + if (code != null && !code.isEmpty() && !isShowAllEntry(code) && + CurrencyUtil.getTradeCurrency(code).isPresent()) { + showAllTradeCurrenciesProperty.set(false); + selectedTradeCurrency = CurrencyUtil.getTradeCurrency(code).get(); + } else { + showAllTradeCurrenciesProperty.set(true); + selectedTradeCurrency = getDefaultTradeCurrency(); + } + tradeCurrencyCode.set(selectedTradeCurrency.getCode()); + } + + abstract TradeCurrency getDefaultTradeCurrency(); + + public void updateSelectedPaymentMethod() { + showAllPaymentMethods = getPaymentMethods().stream().noneMatch(paymentMethod -> + paymentMethod.equals(selectedPaymentMethod)); + } + + abstract String getCurrencyCodeFromPreferences(OfferDirection direction); + + public OpenOffer getOpenOffer(Offer offer) { + return openOfferManager.getOpenOfferById(offer.getId()).orElse(null); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OtherOfferBookView.fxml b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OtherOfferBookView.fxml new file mode 100644 index 0000000000..8d1b543596 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OtherOfferBookView.fxml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OtherOfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OtherOfferBookView.java new file mode 100644 index 0000000000..77a243a1cf --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OtherOfferBookView.java @@ -0,0 +1,65 @@ +/* + * 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 . + */ + +package bisq.desktop.main.offer.offerbook; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.overlays.windows.OfferDetailsWindow; + +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.locale.Res; +import bisq.core.offer.OfferDirection; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.CoinFormatter; + +import bisq.common.config.Config; + +import javax.inject.Inject; +import javax.inject.Named; + +import javafx.scene.layout.GridPane; + +@FxmlView +public class OtherOfferBookView extends OfferBookView { + + @Inject + OtherOfferBookView(OtherOfferBookViewModel model, + Navigation navigation, + OfferDetailsWindow offerDetailsWindow, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + PrivateNotificationManager privateNotificationManager, + @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys, + AccountAgeWitnessService accountAgeWitnessService, + SignedWitnessService signedWitnessService) { + super(model, navigation, offerDetailsWindow, formatter, privateNotificationManager, useDevPrivilegeKeys, accountAgeWitnessService, signedWitnessService); + } + + @Override + protected String getMarketTitle() { + return model.getDirection().equals(OfferDirection.BUY) ? + Res.get("offerbook.availableOffersToBuy", Res.get("shared.otherAssets"), Res.getBaseCurrencyCode()) : + Res.get("offerbook.availableOffersToSell", Res.get("shared.otherAssets"), Res.getBaseCurrencyCode()); + } + + @Override + String getTradeCurrencyCode() { + return model.showAllTradeCurrenciesProperty.get() ? "" : model.getSelectedTradeCurrency().getCode(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OtherOfferBookViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OtherOfferBookViewModel.java new file mode 100644 index 0000000000..45c24006db --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OtherOfferBookViewModel.java @@ -0,0 +1,165 @@ +/* + * 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 . + */ + +package bisq.desktop.main.offer.offerbook; + +import bisq.desktop.Navigation; +import bisq.desktop.main.offer.OfferViewUtil; +import bisq.desktop.util.GUIUtil; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.api.CoreApi; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.locale.CryptoCurrency; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.GlobalSettings; +import bisq.core.locale.TradeCurrency; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; +import bisq.core.offer.OfferFilterService; +import bisq.core.offer.OpenOfferManager; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.ClosedTradableManager; +import bisq.core.user.Preferences; +import bisq.core.user.User; +import bisq.core.util.FormattingUtils; +import bisq.core.util.PriceUtil; +import bisq.core.util.coin.CoinFormatter; + +import bisq.network.p2p.P2PService; + +import com.google.inject.Inject; + +import javax.inject.Named; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.NotNull; + +public class OtherOfferBookViewModel extends OfferBookViewModel { + + @Inject + public OtherOfferBookViewModel(User user, + OpenOfferManager openOfferManager, + OfferBook offerBook, + Preferences preferences, + WalletsSetup walletsSetup, + P2PService p2PService, + PriceFeedService priceFeedService, + ClosedTradableManager closedTradableManager, + AccountAgeWitnessService accountAgeWitnessService, + Navigation navigation, + PriceUtil priceUtil, + OfferFilterService offerFilterService, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + CoreApi coreApi) { + super(user, openOfferManager, offerBook, preferences, walletsSetup, p2PService, priceFeedService, closedTradableManager, accountAgeWitnessService, navigation, priceUtil, offerFilterService, btcFormatter, coreApi); + } + + @Override + void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code) { + if (direction == OfferDirection.BUY) { + preferences.setBuyScreenCryptoCurrencyCode(code); + } else { + preferences.setSellScreenCryptoCurrencyCode(code); + } + } + + @Override + protected ObservableList filterPaymentMethods(ObservableList list, + TradeCurrency selectedTradeCurrency) { + return FXCollections.observableArrayList(list.stream().filter(PaymentMethod::isBlockchain).collect(Collectors.toList())); + } + + @Override + void fillCurrencies(ObservableList tradeCurrencies, + ObservableList allCurrencies) { + + tradeCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); + tradeCurrencies.addAll(preferences.getCryptoCurrenciesAsObservable().stream() + .filter(withoutTopAltcoin()) + .collect(Collectors.toList())); + tradeCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); + + allCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); + allCurrencies.addAll(CurrencyUtil.getAllSortedCryptoCurrencies().stream() + .filter(withoutTopAltcoin()) + .collect(Collectors.toList())); + allCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); + } + + @Override + Predicate getCurrencyAndMethodPredicate(OfferDirection direction, + TradeCurrency selectedTradeCurrency) { + return offerBookListItem -> { + Offer offer = offerBookListItem.getOffer(); + // BUY Altcoin is actually SELL Bitcoin + boolean directionResult = offer.getDirection() == direction; + boolean currencyResult = CurrencyUtil.isCryptoCurrency(offer.getCurrencyCode()) && + ((showAllTradeCurrenciesProperty.get() && + !offer.getCurrencyCode().equals(GUIUtil.TOP_ALTCOIN.getCode()) && + offer.getCurrencyCode().equals(selectedTradeCurrency.getCode()))); + boolean paymentMethodResult = showAllPaymentMethods || + offer.getPaymentMethod().equals(selectedPaymentMethod); + boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); + return directionResult && currencyResult && paymentMethodResult && notMyOfferOrShowMyOffersActivated; + }; + } + + @Override + TradeCurrency getDefaultTradeCurrency() { + TradeCurrency defaultTradeCurrency = GlobalSettings.getDefaultTradeCurrency(); + + if (!CurrencyUtil.isFiatCurrency(defaultTradeCurrency.getCode()) && + !defaultTradeCurrency.equals(GUIUtil.TOP_ALTCOIN) && + hasPaymentAccountForCurrency(defaultTradeCurrency)) { + return defaultTradeCurrency; + } + + ObservableList tradeCurrencies = FXCollections.observableArrayList(getTradeCurrencies()); + if (!tradeCurrencies.isEmpty()) { + // drop show all entry and select first currency with payment account available + tradeCurrencies.remove(0); + List sortedList = tradeCurrencies.stream().sorted((o1, o2) -> + Boolean.compare(!hasPaymentAccountForCurrency(o1), + !hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()); + return sortedList.get(0); + } else { + return OfferViewUtil.getMainCryptoCurrencies().sorted((o1, o2) -> + Boolean.compare(!hasPaymentAccountForCurrency(o1), + !hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()).get(0); + } + } + + @Override + String getCurrencyCodeFromPreferences(OfferDirection direction) { + return direction == OfferDirection.BUY ? preferences.getBuyScreenCryptoCurrencyCode() : + preferences.getSellScreenCryptoCurrencyCode(); + } + + @NotNull + private Predicate withoutTopAltcoin() { + return cryptoCurrency -> + !cryptoCurrency.equals(GUIUtil.TOP_ALTCOIN); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/TopAltcoinOfferBookView.fxml b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/TopAltcoinOfferBookView.fxml new file mode 100644 index 0000000000..ee5f7fc335 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/TopAltcoinOfferBookView.fxml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/TopAltcoinOfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/TopAltcoinOfferBookView.java new file mode 100644 index 0000000000..ab6dbd4778 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/TopAltcoinOfferBookView.java @@ -0,0 +1,75 @@ +/* + * 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 . + */ + +package bisq.desktop.main.offer.offerbook; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.overlays.windows.OfferDetailsWindow; + +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.locale.Res; +import bisq.core.offer.OfferDirection; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.CoinFormatter; + +import bisq.common.config.Config; + +import javax.inject.Inject; +import javax.inject.Named; + +import javafx.scene.layout.GridPane; + +@FxmlView +public class TopAltcoinOfferBookView extends OfferBookView { + + @Inject + TopAltcoinOfferBookView(TopAltcoinOfferBookViewModel model, + Navigation navigation, + OfferDetailsWindow offerDetailsWindow, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + PrivateNotificationManager privateNotificationManager, + @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys, + AccountAgeWitnessService accountAgeWitnessService, + SignedWitnessService signedWitnessService) { + super(model, navigation, offerDetailsWindow, formatter, privateNotificationManager, useDevPrivilegeKeys, accountAgeWitnessService, signedWitnessService); + } + + @Override + protected String getMarketTitle() { + return model.getDirection().equals(OfferDirection.BUY) ? + Res.get("offerbook.availableOffersToBuy", TopAltcoinOfferBookViewModel.TOP_ALTCOIN.getCode(), Res.getBaseCurrencyCode()) : + Res.get("offerbook.availableOffersToSell", TopAltcoinOfferBookViewModel.TOP_ALTCOIN.getCode(), Res.getBaseCurrencyCode()); + } + + @Override + protected void activate() { + model.onSetTradeCurrency(TopAltcoinOfferBookViewModel.TOP_ALTCOIN); + + super.activate(); + + currencyComboBoxContainer.setVisible(false); + currencyComboBoxContainer.setManaged(false); + } + + @Override + String getTradeCurrencyCode() { + return TopAltcoinOfferBookViewModel.TOP_ALTCOIN.getCode(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/TopAltcoinOfferBookViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/TopAltcoinOfferBookViewModel.java new file mode 100644 index 0000000000..d6d0c6e4af --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/TopAltcoinOfferBookViewModel.java @@ -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 . + */ + +package bisq.desktop.main.offer.offerbook; + +import bisq.desktop.Navigation; +import bisq.desktop.util.GUIUtil; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.api.CoreApi; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.locale.TradeCurrency; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; +import bisq.core.offer.OfferFilterService; +import bisq.core.offer.OpenOfferManager; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.ClosedTradableManager; +import bisq.core.user.Preferences; +import bisq.core.user.User; +import bisq.core.util.FormattingUtils; +import bisq.core.util.PriceUtil; +import bisq.core.util.coin.CoinFormatter; + +import bisq.network.p2p.P2PService; + +import com.google.inject.Inject; + +import javax.inject.Named; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class TopAltcoinOfferBookViewModel extends OfferBookViewModel { + + public static TradeCurrency TOP_ALTCOIN = GUIUtil.TOP_ALTCOIN; + + @Inject + public TopAltcoinOfferBookViewModel(User user, + OpenOfferManager openOfferManager, + OfferBook offerBook, + Preferences preferences, + WalletsSetup walletsSetup, + P2PService p2PService, + PriceFeedService priceFeedService, + ClosedTradableManager closedTradableManager, + AccountAgeWitnessService accountAgeWitnessService, + Navigation navigation, + PriceUtil priceUtil, + OfferFilterService offerFilterService, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + CoreApi coreApi) { + super(user, openOfferManager, offerBook, preferences, walletsSetup, p2PService, priceFeedService, closedTradableManager, accountAgeWitnessService, navigation, priceUtil, offerFilterService, btcFormatter, coreApi); + } + + @Override + protected void activate() { + super.activate(); + TOP_ALTCOIN = GUIUtil.TOP_ALTCOIN; + } + + @Override + void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code) { + // No need to store anything as it is just one Altcoin offers anyway + } + + @Override + protected ObservableList filterPaymentMethods(ObservableList list, + TradeCurrency selectedTradeCurrency) { + return FXCollections.observableArrayList(list.stream().filter(PaymentMethod::isBlockchain).collect(Collectors.toList())); + } + + @Override + void fillCurrencies(ObservableList tradeCurrencies, + ObservableList allCurrencies) { + tradeCurrencies.add(TOP_ALTCOIN); + allCurrencies.add(TOP_ALTCOIN); + } + + @Override + Predicate getCurrencyAndMethodPredicate(OfferDirection direction, + TradeCurrency selectedTradeCurrency) { + return offerBookListItem -> { + Offer offer = offerBookListItem.getOffer(); + // BUY Altcoin is actually SELL Bitcoin + boolean directionResult = offer.getDirection() == direction; + boolean currencyResult = offer.getCurrencyCode().equals(TOP_ALTCOIN.getCode()); + boolean paymentMethodResult = showAllPaymentMethods || + offer.getPaymentMethod().equals(selectedPaymentMethod); + boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); + return directionResult && currencyResult && paymentMethodResult && notMyOfferOrShowMyOffersActivated; + }; + } + + @Override + TradeCurrency getDefaultTradeCurrency() { + return TOP_ALTCOIN; + } + + @Override + String getCurrencyCodeFromPreferences(OfferDirection direction) { + return TOP_ALTCOIN.getCode(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java index 0f33533ba0..89a09d5dc4 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -35,6 +35,7 @@ import bisq.core.locale.Res; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferUtil; import bisq.core.payment.PaymentAccount; @@ -75,6 +76,7 @@ import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; +import static bisq.core.payment.payload.PaymentMethod.HAL_CASH_ID; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @@ -212,7 +214,7 @@ class TakeOfferDataModel extends OfferDataModel { this.amount.set(Coin.valueOf(Math.min(offer.getAmount().value, getMaxTradeLimit()))); - securityDeposit = offer.getDirection() == OfferPayload.Direction.SELL ? + securityDeposit = offer.getDirection() == OfferDirection.SELL ? getBuyerSecurityDeposit() : getSellerSecurityDeposit(); @@ -294,7 +296,7 @@ class TakeOfferDataModel extends OfferDataModel { // only local effect. Other trader might see the offer for a few seconds // still (but cannot take it). if (removeOffer) { - offerBook.removeOffer(checkNotNull(offer), tradeManager); + offerBook.removeOffer(checkNotNull(offer)); } //xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId()); // TODO (woodser): this removes address entries for reserved trades before completion. how doesn't this delete the multisig address entry in bisq before completion? @@ -414,7 +416,7 @@ class TakeOfferDataModel extends OfferDataModel { // Getters /////////////////////////////////////////////////////////////////////////////////////////// - OfferPayload.Direction getDirection() { + OfferDirection getDirection() { return offer.getDirection(); } @@ -482,7 +484,7 @@ class TakeOfferDataModel extends OfferDataModel { Volume volumeByAmount = tradePrice.getVolumeByAmount(amount.get()); if (offer.getPaymentMethod().getId().equals(PaymentMethod.HAL_CASH_ID)) volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); - else if (CurrencyUtil.isFiatCurrency(getCurrencyCode())) + else if (offer.isFiatOffer()) volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); volume.set(volumeByAmount); @@ -515,11 +517,11 @@ class TakeOfferDataModel extends OfferDataModel { } boolean isBuyOffer() { - return getDirection() == OfferPayload.Direction.BUY; + return getDirection() == OfferDirection.BUY; } boolean isSellOffer() { - return getDirection() == OfferPayload.Direction.SELL; + return getDirection() == OfferDirection.SELL; } boolean isCryptoCurrency() { @@ -646,8 +648,8 @@ class TakeOfferDataModel extends OfferDataModel { return offer.getSellerSecurityDeposit(); } - public boolean isHalCashAccount() { - return paymentAccount.isHalCashAccount(); + public boolean isUsingHalCashAccount() { + return paymentAccount.hasPaymentMethodWithId(HAL_CASH_ID); } public Coin getTakerFeeInBtc() { diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java index edd746b4a1..7fa6830ff0 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java @@ -33,8 +33,11 @@ import bisq.desktop.components.TitledGroupBg; import bisq.desktop.main.MainView; import bisq.desktop.main.funds.FundsView; import bisq.desktop.main.funds.withdrawal.WithdrawalView; +import bisq.desktop.main.offer.ClosableView; +import bisq.desktop.main.offer.InitializableViewWithTakeOfferData; import bisq.desktop.main.offer.OfferView; import bisq.desktop.main.offer.OfferViewUtil; +import bisq.desktop.main.offer.SelectableView; import bisq.desktop.main.overlays.notifications.Notification; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.GenericMessageWindow; @@ -49,7 +52,6 @@ import bisq.desktop.util.Transitions; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; import bisq.core.payment.FasterPaymentsAccount; import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentMethod; @@ -108,16 +110,17 @@ import javafx.beans.value.ChangeListener; import java.net.URI; import java.io.ByteArrayInputStream; - +import java.util.HashMap; import java.util.concurrent.TimeUnit; import org.jetbrains.annotations.NotNull; +import static bisq.desktop.main.offer.OfferViewUtil.addPayInfoEntry; import static bisq.desktop.util.FormBuilder.*; import static javafx.beans.binding.Bindings.createStringBinding; @FxmlView -public class TakeOfferView extends ActivatableViewAndModel { +public class TakeOfferView extends ActivatableViewAndModel implements ClosableView, InitializableViewWithTakeOfferData, SelectableView { private final Navigation navigation; private final CoinFormatter formatter; private final OfferDetailsWindow offerDetailsWindow; @@ -125,7 +128,8 @@ public class TakeOfferView extends ActivatableViewAndModel paymentAccountWarningDisplayed = new HashMap<>(); private boolean offerDetailsWindowDisplayed, clearXchangeWarningDisplayed, fasterPaymentsWarningDisplayed, takeOfferFromUnsignedAccountWarningDisplayed, cashByMailWarningDisplayed; private SimpleBooleanProperty errorPopupDisplayed; @@ -257,7 +262,6 @@ public class TakeOfferView extends ActivatableViewAndModel GUIUtil.openWebPage("https://bisq.network/faq#6")) + .onAction(() -> GUIUtil.openWebPage("https://bisq.wiki/Frequently_asked_questions#Why_does_Bisq_require_a_security_deposit_in_BTC.3F")) .useIUnderstandButton() .dontShowAgainId(key) .show(); @@ -451,7 +461,7 @@ public class TakeOfferView extends ActivatableViewAndModel new Popup().warning(newValue + "\n\n" + - Res.get("takeOffer.alreadyPaidInFunds")) + Res.get("takeOffer.alreadyPaidInFunds")) .actionButtonTextWithGoTo("navigation.funds.availableForWithdrawal") .onAction(() -> { errorPopupDisplayed.set(true); @@ -615,8 +625,8 @@ public class TakeOfferView extends ActivatableViewAndModel { if (newValue != null) { - new Popup().error(Res.get("takeOffer.error.message", model.errorMessage.get()) + - Res.get("popup.error.tryRestart")) + new Popup().error(Res.get("takeOffer.error.message", model.errorMessage.get()) + "\n\n" + + Res.get("popup.error.tryRestart")) .onClose(() -> { errorPopupDisplayed.set(true); model.resetErrorMessage(); @@ -705,15 +715,7 @@ public class TakeOfferView extends ActivatableViewAndModel, Label, TextField, HBox> paymentAccountTuple = addComboBoxTopLabelTextField(gridPane, - gridRow, Res.get("shared.selectTradingAccount"), - Res.get("shared.paymentMethod"), Layout.FIRST_ROW_DISTANCE); + gridRow, Res.get("shared.chooseTradingAccount"), + Res.get("shared.chooseTradingAccount"), Layout.FIRST_ROW_DISTANCE); paymentAccountsComboBox = paymentAccountTuple.first; HBox.setMargin(paymentAccountsComboBox, new Insets(Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); @@ -752,6 +748,7 @@ public class TakeOfferView extends ActivatableViewAndModel { - cancelButton1.fire(); - }) + .onClose(() -> cancelButton1.fire()) .show(); } else { if (result == -1) { @@ -850,6 +848,7 @@ public class TakeOfferView extends ActivatableViewAndModel model.fundFromSavingsWallet()); @@ -1083,8 +1082,14 @@ public class TakeOfferView extends ActivatableViewAndModel tradeInputBox = getTradeInputBox(vBox, Res.get("createOffer.tradeFee.description")); + final Tuple2 tradeInputBox = getTradeInputBox(hBox, Res.get("createOffer.tradeFee.description")); tradeFeeDescriptionLabel = tradeInputBox.first; @@ -1101,7 +1106,7 @@ public class TakeOfferView extends ActivatableViewAndModel GUIUtil.showTakeOfferFromUnsignedAccountWarning(formatter), 500, TimeUnit.MILLISECONDS); + UserThread.runAfter(() -> GUIUtil.showTakeOfferFromUnsignedAccountWarning(), 500, TimeUnit.MILLISECONDS); } } @@ -1122,6 +1127,11 @@ public class TakeOfferView extends ActivatableViewAndModel getTradeInputBox(HBox amountValueBox, String promptText) { + Label descriptionLabel = new AutoTooltipLabel(promptText); + descriptionLabel.setId("input-description-label"); + descriptionLabel.setPrefWidth(170); + + VBox box = new VBox(); + box.setPadding(new Insets(10, 0, 0, 0)); + box.setSpacing(2); + box.getChildren().addAll(descriptionLabel, amountValueBox); + return new Tuple2<>(descriptionLabel, box); + } + // As we don't use binding here we need to recreate it on mouse over to reflect the current state private GridPane createInfoPopover() { GridPane infoGridPane = new GridPane(); @@ -1147,8 +1169,9 @@ public class TakeOfferView extends ActivatableViewAndModel im addBindings(); addListeners(); + String buyVolumeDescriptionKey = offer.isFiatOffer() ? "createOffer.amountPriceBox.buy.volumeDescription" : + "createOffer.amountPriceBox.buy.volumeDescriptionAltcoin"; + String sellVolumeDescriptionKey = offer.isFiatOffer() ? "createOffer.amountPriceBox.sell.volumeDescription" : + "createOffer.amountPriceBox.sell.volumeDescriptionAltcoin"; + + if (dataModel.getDirection() == OfferDirection.SELL) { + volumeDescriptionLabel.set(Res.get(buyVolumeDescriptionKey, dataModel.getCurrencyCode())); + } else { + volumeDescriptionLabel.set(Res.get(sellVolumeDescriptionKey, dataModel.getCurrencyCode())); + } + amount.set(btcFormatter.formatCoin(dataModel.getAmount().get())); showTransactionPublishedScreen.set(false); @@ -193,13 +205,18 @@ class TakeOfferViewModel extends ActivatableWithDataModel im dataModel.initWithData(offer); this.offer = offer; + String buyAmountDescriptionKey = offer.isFiatOffer() ? "takeOffer.amountPriceBox.buy.amountDescription" : + "takeOffer.amountPriceBox.buy.amountDescriptionAltcoin"; + String sellAmountDescriptionKey = offer.isFiatOffer() ? "takeOffer.amountPriceBox.sell.amountDescription" : + "takeOffer.amountPriceBox.sell.amountDescriptionAltcoin"; + amountDescription = offer.isBuyOffer() - ? Res.get("takeOffer.amountPriceBox.buy.amountDescription") - : Res.get("takeOffer.amountPriceBox.sell.amountDescription"); + ? Res.get(buyAmountDescriptionKey) + : Res.get(sellAmountDescriptionKey); amountRange = btcFormatter.formatCoin(offer.getMinAmount()) + " - " + btcFormatter.formatCoin(offer.getAmount()); price = FormattingUtils.formatPrice(dataModel.tradePrice); - marketPriceMargin = FormattingUtils.formatToPercent(offer.getMarketPriceMargin()); + marketPriceMargin = FormattingUtils.formatToPercent(offer.getMarketPriceMarginPct()); paymentLabel = Res.get("takeOffer.fundsBox.paymentLabel", offer.getShortId()); checkNotNull(dataModel.getAddressEntry(), "dataModel.getAddressEntry() must not be null"); @@ -231,6 +248,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im applyTradeState(); trade.errorMessageProperty().addListener(tradeErrorListener); applyTradeErrorMessage(trade.getErrorMessage()); + takeOfferCompleted.set(true); }); updateButtonDisableState(); @@ -274,7 +292,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im isTradeFeeVisible.setValue(true); tradeFee.set(getFormatterForTakerFee().formatCoin(takerFeeAsCoin)); - tradeFeeInBtcWithFiat.set(FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil, + tradeFeeInBtcWithFiat.set(OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getTakerFeeInBtc(), btcFormatter)); } @@ -306,7 +324,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im maxTradeLimit); dataModel.applyAmount(adjustedAmountForHalCash); amount.set(btcFormatter.formatCoin(dataModel.getAmount().get())); - } else if (CurrencyUtil.isFiatCurrency(dataModel.getCurrencyCode())) { + } else if (dataModel.getOffer().isFiatOffer()) { if (!isAmountEqualMinAmount(dataModel.getAmount().get()) && (!isAmountEqualMaxAmount(dataModel.getAmount().get()))) { // We only apply the rounding if the amount is variable (minAmount is lower as amount). // Otherwise we could get an amount lower then the minAmount set by rounding @@ -329,7 +347,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im amountValidationResult.set(new InputValidator.ValidationResult(false, Res.get("takeOffer.validation.amountLargerThanOfferAmountMinusFee"))); } else if (btcValidator.getMaxTradeLimit() != null && btcValidator.getMaxTradeLimit().value == OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value) { - if (dataModel.getDirection() == OfferPayload.Direction.BUY) { + if (dataModel.getDirection() == OfferDirection.BUY) { new Popup().information(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.seller", btcFormatter.formatCoinWithCode(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT), Res.get("offerbook.warning.newVersionAnnouncement"))) @@ -460,13 +478,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im /////////////////////////////////////////////////////////////////////////////////////////// private void addBindings() { - volume.bind(createStringBinding(() -> DisplayUtils.formatVolume(dataModel.volume.get()), dataModel.volume)); - - if (dataModel.getDirection() == OfferPayload.Direction.SELL) { - volumeDescriptionLabel.set(Res.get("createOffer.amountPriceBox.buy.volumeDescription", dataModel.getCurrencyCode())); - } else { - volumeDescriptionLabel.set(Res.get("createOffer.amountPriceBox.sell.volumeDescription", dataModel.getCurrencyCode())); - } + volume.bind(createStringBinding(() -> VolumeUtil.formatVolume(dataModel.volume.get()), dataModel.volume)); totalToPay.bind(createStringBinding(() -> btcFormatter.formatCoinWithCode(dataModel.getTotalToPayAsCoin().get()), dataModel.getTotalToPayAsCoin())); } @@ -598,9 +610,9 @@ class TakeOfferViewModel extends ActivatableWithDataModel im long maxTradeLimit = dataModel.getMaxTradeLimit(); Price price = dataModel.tradePrice; if (price != null) { - if (dataModel.isHalCashAccount()) { + if (dataModel.isUsingHalCashAccount()) { amount = CoinUtil.getAdjustedAmountForHalCash(amount, price, maxTradeLimit); - } else if (CurrencyUtil.isFiatCurrency(dataModel.getCurrencyCode()) + } else if (dataModel.getOffer().isFiatOffer() && !isAmountEqualMinAmount(amount) && !isAmountEqualMaxAmount(amount)) { // We only apply the rounding if the amount is variable (minAmount is lower as amount). // Otherwise we could get an amount lower then the minAmount set by rounding @@ -628,11 +640,11 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } boolean isSeller() { - return dataModel.getDirection() == OfferPayload.Direction.BUY; + return dataModel.getDirection() == OfferDirection.BUY; } public boolean isSellingToAnUnsignedAccount(Offer offer) { - if (offer.getDirection() == OfferPayload.Direction.BUY && + if (offer.getDirection() == OfferDirection.BUY && PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode())) { // considered risky when either UNSIGNED, PEER_INITIAL, or BANNED (see #5343) return accountAgeWitnessService.getSignState(offer) == AccountAgeWitnessService.SignState.UNSIGNED || @@ -671,14 +683,18 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } String getTradeAmount() { - return btcFormatter.formatCoinWithCode(dataModel.getAmount().get()); + return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, + dataModel.getAmount().get(), + btcFormatter); } public String getSecurityDepositInfo() { - return btcFormatter.formatCoinWithCode(dataModel.getSecurityDeposit()) + - GUIUtil.getPercentageOfTradeAmount(dataModel.getSecurityDeposit(), - dataModel.getAmount().get(), - Restrictions.getMinBuyerSecurityDepositAsCoin()); + return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, + dataModel.getSecurityDeposit(), + dataModel.getAmount().get(), + btcFormatter, + Restrictions.getMinBuyerSecurityDepositAsCoin() + ); } public String getSecurityDepositWithCode() { @@ -686,7 +702,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } public String getTradeFee() { - return FeeUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, + return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getTakerFeeInBtc(), dataModel.getAmount().get(), btcFormatter, @@ -704,7 +720,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } public String getTxFee() { - return FeeUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, + return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getTotalTxFee(), dataModel.getAmount().get(), btcFormatter, diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ContractWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ContractWindow.java index 59cb16ff13..5c8a6a462e 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ContractWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ContractWindow.java @@ -38,6 +38,7 @@ import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.support.dispute.refund.RefundManager; import bisq.core.trade.Contract; import bisq.core.util.FormattingUtils; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.CoinFormatter; import bisq.network.p2p.NodeAddress; @@ -59,7 +60,6 @@ import javafx.stage.Window; import javafx.scene.Scene; import javafx.scene.control.Button; -import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCode; @@ -71,6 +71,7 @@ import java.util.List; import lombok.extern.slf4j.Slf4j; +import static bisq.desktop.util.DisplayUtils.getAccountWitnessDescription; import static bisq.desktop.util.FormBuilder.*; @Slf4j @@ -149,19 +150,21 @@ public class ContractWindow extends Overlay { rows++; addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("contractWindow.title")); - addConfirmationLabelTextFieldWithCopyIcon(gridPane, rowIndex, Res.get("shared.offerId"), offer.getId(), - Layout.TWICE_FIRST_ROW_DISTANCE).second.setMouseTransparent(false); - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("contractWindow.dates"), + addConfirmationLabelTextField(gridPane, rowIndex, Res.get("shared.offerId"), offer.getId(), + Layout.TWICE_FIRST_ROW_DISTANCE); + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.dates"), DisplayUtils.formatDateTime(offer.getDate()) + " / " + DisplayUtils.formatDateTime(dispute.getTradeDate())); String currencyCode = offer.getCurrencyCode(); - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.offerType"), - DisplayUtils.getDirectionBothSides(offer.getDirection(), currencyCode)); - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.tradePrice"), - FormattingUtils.formatPrice(contract.getTradePrice())); - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.tradeAmount"), + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.offerType"), + DisplayUtils.getDirectionBothSides(offer.getDirection())); + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradePrice"), + FormattingUtils.formatPrice(contract.getPrice())); + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradeAmount"), formatter.formatCoinWithCode(contract.getTradeAmount())); - addConfirmationLabelLabel(gridPane, ++rowIndex, DisplayUtils.formatVolumeLabel(currencyCode, ":"), - DisplayUtils.formatVolumeWithCode(contract.getTradeVolume())); + addConfirmationLabelTextField(gridPane, + ++rowIndex, + VolumeUtil.formatVolumeLabel(currencyCode, ":"), + VolumeUtil.formatVolumeWithCode(contract.getTradeVolume())); String securityDeposit = Res.getWithColAndCap("shared.buyer") + " " + formatter.formatCoinWithCode(offer.getBuyerSecurityDeposit()) + @@ -169,30 +172,44 @@ public class ContractWindow extends Overlay { Res.getWithColAndCap("shared.seller") + " " + formatter.formatCoinWithCode(offer.getSellerSecurityDeposit()); - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("contractWindow.btcAddresses"), - contract.getBuyerPayoutAddressString() + " / " + - contract.getSellerPayoutAddressString()).second.setMouseTransparent(false); - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("contractWindow.onions"), + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); + addConfirmationLabelTextField(gridPane, + ++rowIndex, + Res.get("contractWindow.btcAddresses"), + contract.getBuyerPayoutAddressString() + " / " + contract.getSellerPayoutAddressString()); + addConfirmationLabelTextField(gridPane, + ++rowIndex, + Res.get("contractWindow.onions"), contract.getBuyerNodeAddress().getFullAddress() + " / " + contract.getSellerNodeAddress().getFullAddress()); - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("contractWindow.accountAge"), - getAccountAge(dispute.getBuyerPaymentAccountPayload(), contract.getBuyerPubKeyRing(), offer.getCurrencyCode()) + " / " + - getAccountAge(dispute.getSellerPaymentAccountPayload(), contract.getSellerPubKeyRing(), offer.getCurrencyCode())); + addConfirmationLabelTextField(gridPane, + ++rowIndex, + Res.get("contractWindow.accountAge"), + getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), dispute.getBuyerPaymentAccountPayload(), contract.getBuyerPubKeyRing()) + " / " + + getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), dispute.getSellerPaymentAccountPayload(), contract.getSellerPubKeyRing())); DisputeManager> disputeManager = getDisputeManager(dispute); String nrOfDisputesAsBuyer = disputeManager != null ? disputeManager.getNrOfDisputes(true, contract) : ""; String nrOfDisputesAsSeller = disputeManager != null ? disputeManager.getNrOfDisputes(false, contract) : ""; - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("contractWindow.numDisputes"), + addConfirmationLabelTextField(gridPane, + ++rowIndex, + Res.get("contractWindow.numDisputes"), nrOfDisputesAsBuyer + " / " + nrOfDisputesAsSeller); - - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.buyer")), - dispute.getBuyerPaymentAccountPayload().getPaymentDetails()).second.setMouseTransparent(false); - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.seller")), - dispute.getSellerPaymentAccountPayload().getPaymentDetails()).second.setMouseTransparent(false); + addConfirmationLabelTextField(gridPane, + ++rowIndex, + Res.get("shared.paymentDetails", Res.get("shared.buyer")), + dispute.getBuyerPaymentAccountPayload() != null + ? dispute.getBuyerPaymentAccountPayload().getPaymentDetails() + : "NA"); + addConfirmationLabelTextField(gridPane, + ++rowIndex, + Res.get("shared.paymentDetails", Res.get("shared.seller")), + dispute.getSellerPaymentAccountPayload() != null + ? dispute.getSellerPaymentAccountPayload().getPaymentDetails() + : "NA"); String title = ""; - String agentKeyBaseUserName = ""; + String agentMatrixUserName = ""; if (dispute.getSupportType() != null) { switch (dispute.getSupportType()) { case ARBITRATION: @@ -200,24 +217,24 @@ public class ContractWindow extends Overlay { break; case MEDIATION: throw new RuntimeException("Mediation type not adapted to XMR"); -// agentKeyBaseUserName = DisputeAgentLookupMap.getKeyBaseUserName(contract.getMediatorNodeAddress().getFullAddress()); +// agentMatrixUserName = DisputeAgentLookupMap.getMatrixUserName(contract.getMediatorNodeAddress().getFullAddress()); // title = Res.get("shared.selectedMediator"); // break; case TRADE: break; case REFUND: throw new RuntimeException("Refund type not adapted to XMR"); - //agentKeyBaseUserName = DisputeAgentLookupMap.getKeyBaseUserName(contract.getRefundAgentNodeAddress().getFullAddress()); - //title = Res.get("shared.selectedRefundAgent"); - //break; +// agentMatrixUserName = DisputeAgentLookupMap.getMatrixUserName(contract.getRefundAgentNodeAddress().getFullAddress()); +// title = Res.get("shared.selectedRefundAgent"); +// break; } } if (disputeManager != null) { NodeAddress agentNodeAddress = disputeManager.getAgentNodeAddress(dispute); if (agentNodeAddress != null) { - String value = agentKeyBaseUserName + " (" + agentNodeAddress.getFullAddress() + ")"; - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, title, value); + String value = agentMatrixUserName + " (" + agentNodeAddress.getFullAddress() + ")"; + addConfirmationLabelTextField(gridPane, ++rowIndex, title, value); } } @@ -230,19 +247,18 @@ public class ContractWindow extends Overlay { countries = CountryUtil.getCodesString(acceptedCountryCodes); tooltip = new Tooltip(CountryUtil.getNamesByCodesString(acceptedCountryCodes)); } - Label acceptedCountries = addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.acceptedTakerCountries"), countries).second; - if (tooltip != null) acceptedCountries.setTooltip(new Tooltip()); + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.acceptedTakerCountries"), countries) + .second.setTooltip(tooltip); } if (showAcceptedBanks) { if (offer.getPaymentMethod().equals(PaymentMethod.SAME_BANK)) { - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.bankName"), acceptedBanks.get(0)); + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.bankName"), acceptedBanks.get(0)); } else if (offer.getPaymentMethod().equals(PaymentMethod.SPECIFIC_BANKS)) { String value = Joiner.on(", ").join(acceptedBanks); Tooltip tooltip = new Tooltip(Res.get("shared.acceptedBanks") + value); - Label acceptedBanksTextField = addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.acceptedBanks"), value).second; - acceptedBanksTextField.setMouseTransparent(false); - acceptedBanksTextField.setTooltip(tooltip); + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.acceptedBanks"), value) + .second.setTooltip(tooltip); } } @@ -256,7 +272,7 @@ public class ContractWindow extends Overlay { addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxId"), dispute.getDelayedPayoutTxId()); if (dispute.getDonationAddressOfDelayedPayoutTx() != null) { - addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxReceiverAddress"), + addLabelExplorerAddressTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxReceiverAddress"), dispute.getDonationAddressOfDelayedPayoutTx()); } @@ -331,14 +347,4 @@ public class ContractWindow extends Overlay { } return null; } - - private String getAccountAge(PaymentAccountPayload paymentAccountPayload, - PubKeyRing pubKeyRing, - String currencyCode) { - long age = accountAgeWitnessService.getAccountAge(paymentAccountPayload, pubKeyRing); - return CurrencyUtil.isFiatCurrency(currencyCode) ? - age > -1 ? Res.get("peerInfoIcon.tooltip.age", DisplayUtils.formatAccountAge(age)) : - Res.get("peerInfoIcon.tooltip.unknownAge") : - ""; - } } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index 31f8428917..43dc098b70 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -42,6 +42,7 @@ import bisq.core.support.dispute.refund.RefundManager; import bisq.core.trade.Contract; import bisq.core.util.FormattingUtils; import bisq.core.util.ParsingUtils; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.CoinFormatter; import bisq.common.UserThread; @@ -283,9 +284,9 @@ public class DisputeSummaryWindow extends Overlay { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.tradeAmount"), formatter.formatCoinWithCode(contract.getTradeAmount())); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.tradePrice"), - FormattingUtils.formatPrice(contract.getTradePrice())); + FormattingUtils.formatPrice(contract.getPrice())); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.tradeVolume"), - DisplayUtils.formatVolumeWithCode(contract.getTradeVolume())); + VolumeUtil.formatVolumeWithCode(contract.getTradeVolume())); String securityDeposit = Res.getWithColAndCap("shared.buyer") + " " + formatter.formatCoinWithCode(contract.getOfferPayload().getBuyerSecurityDeposit()) + diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/OfferDetailsWindow.java index e0cf1adf4e..ae57d3f457 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -33,12 +33,14 @@ import bisq.core.locale.CountryUtil; import bisq.core.locale.Res; import bisq.core.monetary.Price; import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferUtil; import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentMethod; import bisq.core.user.User; import bisq.core.util.FormattingUtils; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.CoinFormatter; import bisq.common.crypto.KeyRing; @@ -175,7 +177,7 @@ public class OfferDetailsWindow extends Overlay { if (isF2F) rows++; - boolean showXmrAutoConf = offer.isXmr() && offer.getDirection() == OfferPayload.Direction.SELL; + boolean showXmrAutoConf = offer.isXmr() && offer.getDirection() == OfferDirection.SELL; if (showXmrAutoConf) { rows++; } @@ -184,7 +186,7 @@ public class OfferDetailsWindow extends Overlay { String fiatDirectionInfo = ""; String btcDirectionInfo = ""; - OfferPayload.Direction direction = offer.getDirection(); + OfferDirection direction = offer.getDirection(); String currencyCode = offer.getCurrencyCode(); String offerTypeLabel = Res.get("shared.offerType"); String toReceive = " " + Res.get("shared.toReceive"); @@ -193,35 +195,35 @@ public class OfferDetailsWindow extends Overlay { if (takeOfferHandlerOptional.isPresent()) { addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel, DisplayUtils.getDirectionForTakeOffer(direction, currencyCode), firstRowDistance); - fiatDirectionInfo = direction == OfferPayload.Direction.BUY ? toReceive : toSpend; - btcDirectionInfo = direction == OfferPayload.Direction.SELL ? toReceive : toSpend; + fiatDirectionInfo = direction == OfferDirection.BUY ? toReceive : toSpend; + btcDirectionInfo = direction == OfferDirection.SELL ? toReceive : toSpend; } else if (placeOfferHandlerOptional.isPresent()) { addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel, DisplayUtils.getOfferDirectionForCreateOffer(direction, currencyCode), firstRowDistance); - fiatDirectionInfo = direction == OfferPayload.Direction.SELL ? toReceive : toSpend; - btcDirectionInfo = direction == OfferPayload.Direction.BUY ? toReceive : toSpend; + fiatDirectionInfo = direction == OfferDirection.SELL ? toReceive : toSpend; + btcDirectionInfo = direction == OfferDirection.BUY ? toReceive : toSpend; } else { addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel, - DisplayUtils.getDirectionBothSides(direction, currencyCode), firstRowDistance); + DisplayUtils.getDirectionBothSides(direction), firstRowDistance); } String btcAmount = Res.get("shared.btcAmount"); if (takeOfferHandlerOptional.isPresent()) { addConfirmationLabelLabel(gridPane, ++rowIndex, btcAmount + btcDirectionInfo, formatter.formatCoinWithCode(tradeAmount)); - addConfirmationLabelLabel(gridPane, ++rowIndex, DisplayUtils.formatVolumeLabel(currencyCode) + fiatDirectionInfo, - DisplayUtils.formatVolumeWithCode(offer.getVolumeByAmount(tradeAmount))); + addConfirmationLabelLabel(gridPane, ++rowIndex, VolumeUtil.formatVolumeLabel(currencyCode) + fiatDirectionInfo, + VolumeUtil.formatVolumeWithCode(offer.getVolumeByAmount(tradeAmount))); } else { addConfirmationLabelLabel(gridPane, ++rowIndex, btcAmount + btcDirectionInfo, formatter.formatCoinWithCode(offer.getAmount())); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.minBtcAmount"), formatter.formatCoinWithCode(offer.getMinAmount())); - String volume = DisplayUtils.formatVolumeWithCode(offer.getVolume()); + String volume = VolumeUtil.formatVolumeWithCode(offer.getVolume()); String minVolume = ""; if (offer.getVolume() != null && offer.getMinVolume() != null && !offer.getVolume().equals(offer.getMinVolume())) - minVolume = " " + Res.get("offerDetailsWindow.min", DisplayUtils.formatVolumeWithCode(offer.getMinVolume())); + minVolume = " " + Res.get("offerDetailsWindow.min", VolumeUtil.formatVolumeWithCode(offer.getMinVolume())); addConfirmationLabelLabel(gridPane, ++rowIndex, - DisplayUtils.formatVolumeLabel(currencyCode) + fiatDirectionInfo, volume + minVolume); + VolumeUtil.formatVolumeLabel(currencyCode) + fiatDirectionInfo, volume + minVolume); } String priceLabel = Res.get("shared.price"); @@ -232,7 +234,7 @@ public class OfferDetailsWindow extends Overlay { if (offer.isUseMarketBasedPrice()) { addConfirmationLabelLabel(gridPane, ++rowIndex, priceLabel, FormattingUtils.formatPrice(price) + " " + Res.get("offerDetailsWindow.distance", - FormattingUtils.formatPercentagePrice(offer.getMarketPriceMargin()))); + FormattingUtils.formatPercentagePrice(offer.getMarketPriceMarginPct()))); } else { addConfirmationLabelLabel(gridPane, ++rowIndex, priceLabel, FormattingUtils.formatPrice(price)); } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/SignPaymentAccountsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SignPaymentAccountsWindow.java index 566cec7bda..27dd7c21c4 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/SignPaymentAccountsWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SignPaymentAccountsWindow.java @@ -171,7 +171,7 @@ public class SignPaymentAccountsWindow extends Overlay getPaymentMethods() { return PaymentMethod.getPaymentMethods().stream() - .filter(paymentMethod -> !paymentMethod.isAsset()) + .filter(PaymentMethod::isFiat) .filter(PaymentMethod::hasChargebackRisk) .collect(Collectors.toList()); } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/SwiftPaymentDetails.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SwiftPaymentDetails.java new file mode 100644 index 0000000000..dc4d74ba0d --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SwiftPaymentDetails.java @@ -0,0 +1,118 @@ +/* + * 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 . + */ + +package bisq.desktop.main.overlays.windows; + +import bisq.desktop.main.overlays.Overlay; + +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; +import bisq.core.payment.payload.SwiftAccountPayload; +import bisq.core.trade.Trade; +import bisq.core.util.VolumeUtil; + +import javafx.scene.control.Label; + +import javafx.geometry.Insets; + +import java.util.ArrayList; +import java.util.List; + +import static bisq.common.util.Utilities.cleanString; +import static bisq.common.util.Utilities.copyToClipboard; +import static bisq.core.payment.payload.SwiftAccountPayload.*; +import static bisq.desktop.util.FormBuilder.addConfirmationLabelLabel; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; + +public class SwiftPaymentDetails extends Overlay { + private final SwiftAccountPayload payload; + private final Trade trade; + private final List copyToClipboardData = new ArrayList<>(); + + public SwiftPaymentDetails(SwiftAccountPayload swiftAccountPayload, Trade trade) { + this.payload = swiftAccountPayload; + this.trade = trade; + } + + @Override + public void show() { + rowIndex = -1; + width = 918; + createGridPane(); + addContent(); + addButtons(); + display(); + } + + @Override + protected void cleanup() { + } + + @Override + protected void createGridPane() { + super.createGridPane(); + gridPane.setPadding(new Insets(35, 40, 30, 40)); + gridPane.getStyleClass().add("grid-pane"); + } + + private void addContent() { + int rows = payload.usesIntermediaryBank() ? 22 : 16; + addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("payment.swift.headline")); + + gridPane.add(new Label(""), 0, ++rowIndex); // spacer + addLabelsAndCopy(Res.get("portfolio.pending.step2_buyer.amountToTransfer"), + VolumeUtil.formatVolumeWithCode(trade.getVolume())); + addLabelsAndCopy(Res.get(SWIFT_CODE + BANKPOSTFIX), payload.getBankSwiftCode()); + addLabelsAndCopy(Res.get(SNAME + BANKPOSTFIX), payload.getBankName()); + addLabelsAndCopy(Res.get(BRANCH + BANKPOSTFIX), payload.getBankBranch()); + addLabelsAndCopy(Res.get(ADDRESS + BANKPOSTFIX), cleanString(payload.getBankAddress())); + addLabelsAndCopy(Res.get(COUNTRY + BANKPOSTFIX), CountryUtil.getNameAndCode(payload.getBankCountryCode())); + + if (payload.usesIntermediaryBank()) { + gridPane.add(new Label(""), 0, ++rowIndex); // spacer + addLabelsAndCopy(Res.get(SWIFT_CODE + INTERMEDIARYPOSTFIX), payload.getIntermediarySwiftCode()); + addLabelsAndCopy(Res.get(SNAME + INTERMEDIARYPOSTFIX), payload.getIntermediaryName()); + addLabelsAndCopy(Res.get(BRANCH + INTERMEDIARYPOSTFIX), payload.getIntermediaryBranch()); + addLabelsAndCopy(Res.get(ADDRESS + INTERMEDIARYPOSTFIX), cleanString(payload.getIntermediaryAddress())); + addLabelsAndCopy(Res.get(COUNTRY + INTERMEDIARYPOSTFIX), + CountryUtil.getNameAndCode(payload.getIntermediaryCountryCode())); + } + + gridPane.add(new Label(""), 0, ++rowIndex); // spacer + addLabelsAndCopy(Res.get("payment.account.owner"), payload.getBeneficiaryName()); + addLabelsAndCopy(Res.get(SWIFT_ACCOUNT), payload.getBeneficiaryAccountNr()); + addLabelsAndCopy(Res.get(ADDRESS + BENEFICIARYPOSTFIX), cleanString(payload.getBeneficiaryAddress())); + addLabelsAndCopy(Res.get(PHONE + BENEFICIARYPOSTFIX), payload.getBeneficiaryPhone()); + addLabelsAndCopy(Res.get("payment.account.city"), payload.getBeneficiaryCity()); + addLabelsAndCopy(Res.get("payment.country"), CountryUtil.getNameAndCode(payload.getBankCountryCode())); + addLabelsAndCopy(Res.get("payment.shared.extraInfo"), cleanString(payload.getSpecialInstructions())); + + actionButtonText(Res.get("shared.copyToClipboard")); + onAction(() -> { + StringBuilder work = new StringBuilder(); + for (String s : copyToClipboardData) { + work.append(s).append(System.lineSeparator()); + } + copyToClipboard(work.toString()); + }); + } + + private void addLabelsAndCopy(String title, String value) { + addConfirmationLabelLabel(gridPane, ++rowIndex, title, value); + copyToClipboardData.add(title + " : " + value); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TacWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TacWindow.java index d4c731bafc..4dc032f42c 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TacWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TacWindow.java @@ -72,7 +72,7 @@ public class TacWindow extends Overlay { "2. The user is responsible for using the software in compliance with local laws. Don't use the software if using it is not legal in your jurisdiction.\n\n" + - "3. Any " + Res.getBaseCurrencyName() + " market prices, network fee estimates, or other data obtained from servers operated by Bisq provided on an 'as is, as available' basis without representation or warranty of any kind. It is your responsibility to verify any data provided in regards to inaccuracies or omissions.\n\n" + + "3. Any " + Res.getBaseCurrencyName() + " market prices, network fee estimates, or other data obtained from servers operated by the Bisq DAO is provided on an 'as is, as available' basis without representation or warranty of any kind. It is your responsibility to verify any data provided in regards to inaccuracies or omissions.\n\n" + "4. Any Fiat payment method carries a potential risk for bank chargeback. By accepting the \"User Agreement\" the user confirms " + "to be aware of those risks and in no case will claim legal responsibility to the authors or copyright holders of the software.\n\n" + @@ -89,11 +89,11 @@ public class TacWindow extends Overlay { " - If either (or both) traders do not accept the mediator's suggested payout, traders can open a refund request from an arbitrator after 10 days in case of altcoin trades\n" + " and 20 days for fiat trades.\n" + " - You should only open a refund request from an arbitrator if you think the mediator's suggested payout is unfair, or if your trading peer is unresponsive.\n" + - " - Opening a refund request from an arbitrator triggers the delayed payout transaction, sending all funds from the deposit transaction to the Bisq receiver\n" + + " - Opening a refund request from an arbitrator triggers the delayed payout transaction, sending all funds from the deposit transaction to the Bisq DAO receiver\n" + " address ('collateral for refund to avoid scamming the refund process'). At this point, the arbitrator will re-investigate the case and personally refund \n" + " (at their discretion) the trader who requested arbitration.\n" + " - The arbitrator may charge a small fee (max. the traders security deposit) as compensation for their work.\n" + - " - The arbitrator will then make a reimbursement request to the Bisq to get reimbursed for the refund they paid to the trader.\n\n" + + " - The arbitrator will then make a reimbursement request to the Bisq DAO to get reimbursed for the refund they paid to the trader.\n\n" + "For more details and a general overview please read the full documentation about dispute resolution."; message(text); actionButtonText(Res.get("tacWindow.agree")); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java index 5392a18802..c50d2d6a69 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java @@ -28,17 +28,17 @@ import bisq.desktop.util.Layout; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.btc.wallet.BtcWalletService; -import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.offer.Offer; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.support.dispute.agent.DisputeAgentLookupMap; import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.trade.TradeManager; import bisq.core.trade.Contract; import bisq.core.trade.Trade; -import bisq.core.trade.TradeManager; import bisq.core.trade.txproof.AssetTxProofResult; import bisq.core.util.FormattingUtils; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.CoinFormatter; import bisq.network.p2p.NodeAddress; @@ -46,6 +46,9 @@ import bisq.network.p2p.NodeAddress; import bisq.common.UserThread; import bisq.common.util.Tuple3; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.Utils; + import javax.inject.Inject; import javax.inject.Named; @@ -73,6 +76,7 @@ import javafx.beans.value.ChangeListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static bisq.desktop.util.DisplayUtils.getAccountWitnessDescription; import static bisq.desktop.util.FormBuilder.*; import static com.google.common.base.Preconditions.checkNotNull; @@ -151,26 +155,26 @@ public class TradeDetailsWindow extends Overlay { String toSpend = " " + Res.get("shared.toSpend"); String offerType = Res.get("shared.offerType"); if (tradeManager.isBuyer(offer)) { - addConfirmationLabelLabel(gridPane, rowIndex, offerType, + addConfirmationLabelTextField(gridPane, rowIndex, offerType, DisplayUtils.getDirectionForBuyer(myOffer, offer.getCurrencyCode()), Layout.TWICE_FIRST_ROW_DISTANCE); fiatDirectionInfo = toSpend; btcDirectionInfo = toReceive; } else { - addConfirmationLabelLabel(gridPane, rowIndex, offerType, + addConfirmationLabelTextField(gridPane, rowIndex, offerType, DisplayUtils.getDirectionForSeller(myOffer, offer.getCurrencyCode()), Layout.TWICE_FIRST_ROW_DISTANCE); fiatDirectionInfo = toReceive; btcDirectionInfo = toSpend; } - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.btcAmount") + btcDirectionInfo, - formatter.formatCoinWithCode(trade.getTradeAmount())); - addConfirmationLabelLabel(gridPane, ++rowIndex, - DisplayUtils.formatVolumeLabel(offer.getCurrencyCode()) + fiatDirectionInfo, - DisplayUtils.formatVolumeWithCode(trade.getTradeVolume())); - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.tradePrice"), - FormattingUtils.formatPrice(trade.getTradePrice())); + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.btcAmount") + btcDirectionInfo, + formatter.formatCoinWithCode(trade.getAmount())); + addConfirmationLabelTextField(gridPane, ++rowIndex, + VolumeUtil.formatVolumeLabel(offer.getCurrencyCode()) + fiatDirectionInfo, + VolumeUtil.formatVolumeWithCode(trade.getVolume())); + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradePrice"), + FormattingUtils.formatPrice(trade.getPrice())); String paymentMethodText = Res.get(offer.getPaymentMethod().getId()); - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.paymentMethod"), paymentMethodText); + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentMethod"), paymentMethodText); // second group rows = 7; @@ -196,16 +200,10 @@ public class TradeDetailsWindow extends Overlay { trade.getAssetTxProofResult() != null && trade.getAssetTxProofResult() != AssetTxProofResult.UNDEFINED; - if (trade.getTakerFeeTxId() != null) - rows++; - if (trade.getMakerDepositTx() != null) - rows++; - if (trade.getTakerDepositTx() != null) - rows++; if (trade.getPayoutTx() != null) rows++; - boolean showDisputedTx = arbitrationManager.findDispute(trade.getId()).isPresent() && - arbitrationManager.findDispute(trade.getId()).get().getDisputePayoutTxId() != null; + boolean showDisputedTx = arbitrationManager.findOwnDispute(trade.getId()).isPresent() && + arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId() != null; if (showDisputedTx) rows++; if (trade.hasFailed()) @@ -216,9 +214,9 @@ public class TradeDetailsWindow extends Overlay { rows++; addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.GROUP_DISTANCE); - addConfirmationLabelTextFieldWithCopyIcon(gridPane, rowIndex, Res.get("shared.tradeId"), + addConfirmationLabelTextField(gridPane, rowIndex, Res.get("shared.tradeId"), trade.getId(), Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradeDate"), + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradeDate"), DisplayUtils.formatDateTime(trade.getDate())); String securityDeposit = Res.getWithColAndCap("shared.buyer") + " " + @@ -227,62 +225,51 @@ public class TradeDetailsWindow extends Overlay { Res.getWithColAndCap("shared.seller") + " " + formatter.formatCoinWithCode(offer.getSellerSecurityDeposit()); - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); String txFee = Res.get("shared.makerTxFee", formatter.formatCoinWithCode(offer.getTxFee())) + " / " + Res.get("shared.takerTxFee", formatter.formatCoinWithCode(trade.getTxFee().multiply(3))); - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.txFee"), txFee); + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.txFee"), txFee); NodeAddress arbitratorNodeAddress = trade.getArbitratorNodeAddress(); if (arbitratorNodeAddress != null) { - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.agentAddresses"), arbitratorNodeAddress.getFullAddress()); } if (trade.getTradingPeerNodeAddress() != null) - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradingPeersOnion"), + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradingPeersOnion"), trade.getTradingPeerNodeAddress().getFullAddress()); if (showXmrProofResult) { // As the window is already overloaded we replace the tradingPeersPubKeyHash field with the auto-conf state // if XMR is the currency - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("portfolio.pending.step3_seller.autoConf.status.label"), GUIUtil.getProofResultAsString(trade.getAssetTxProofResult())); } if (contract != null) { + buyersAccountAge = getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), buyerPaymentAccountPayload, contract.getBuyerPubKeyRing()); + sellersAccountAge = getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), sellerPaymentAccountPayload, contract.getSellerPubKeyRing()); if (buyerPaymentAccountPayload != null) { String paymentDetails = buyerPaymentAccountPayload.getPaymentDetails(); - long age = accountAgeWitnessService.getAccountAge(buyerPaymentAccountPayload, contract.getBuyerPubKeyRing()); - buyersAccountAge = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) ? - age > -1 ? Res.get("peerInfoIcon.tooltip.age", DisplayUtils.formatAccountAge(age)) : - Res.get("peerInfoIcon.tooltip.unknownAge") : - ""; - - String postFix = buyersAccountAge.isEmpty() ? "" : " / " + buyersAccountAge; - TextFieldWithCopyIcon tf = addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, + String postFix = " / " + buyersAccountAge; + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.buyer")), - paymentDetails + postFix).second; - tf.setTooltip(new Tooltip(tf.getText())); + paymentDetails + postFix).second.setTooltip(new Tooltip(paymentDetails + postFix)); } if (sellerPaymentAccountPayload != null) { String paymentDetails = sellerPaymentAccountPayload.getPaymentDetails(); - long age = accountAgeWitnessService.getAccountAge(sellerPaymentAccountPayload, contract.getSellerPubKeyRing()); - sellersAccountAge = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) ? - age > -1 ? Res.get("peerInfoIcon.tooltip.age", DisplayUtils.formatAccountAge(age)) : - Res.get("peerInfoIcon.tooltip.unknownAge") : - ""; - String postFix = sellersAccountAge.isEmpty() ? "" : " / " + sellersAccountAge; - TextFieldWithCopyIcon tf = addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, + String postFix = " / " + sellersAccountAge; + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.seller")), - paymentDetails + postFix).second; - tf.setTooltip(new Tooltip(tf.getText())); + paymentDetails + postFix).second.setTooltip(new Tooltip(paymentDetails + postFix)); } if (buyerPaymentAccountPayload == null && sellerPaymentAccountPayload == null) - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.paymentMethod"), + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentMethod"), Res.get(contract.getPaymentMethodId())); } @@ -290,18 +277,18 @@ public class TradeDetailsWindow extends Overlay { addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.takerFeeTxId"), trade.getTakerFeeTxId()); if (trade.getMakerDepositTx() != null) - addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.depositTransactionId"), // TODO (woodser): separate UI labels for deposit tx ids - trade.getMakerDepositTx().getHash()); - if (trade.getTakerDepositTx() != null) - addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.depositTransactionId"), // TODO (woodser): separate UI labels for deposit tx ids - trade.getTakerDepositTx().getHash()); + addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.depositTransactionId"), // TODO (woodser): separate UI labels for deposit tx ids + trade.getMakerDepositTx().getHash()); + if (trade.getTakerDepositTx() != null) + addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.depositTransactionId"), // TODO (woodser): separate UI labels for deposit tx ids + trade.getTakerDepositTx().getHash()); if (trade.getPayoutTx() != null) addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.payoutTxId"), trade.getPayoutTx().getHash()); if (showDisputedTx) addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.disputedPayoutTxId"), - arbitrationManager.findDispute(trade.getId()).get().getDisputePayoutTxId()); + arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId()); if (trade.hasFailed()) { textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("shared.errorMessage"), "", 0).second; @@ -319,7 +306,7 @@ public class TradeDetailsWindow extends Overlay { textArea.scrollTopProperty().addListener(changeListener); textArea.setScrollTop(30); - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradeState"), trade.getState().getPhase().name()); + addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradeState"), trade.getPhase().name()); } Tuple3 tuple = add2ButtonsWithBox(gridPane, ++rowIndex, @@ -340,18 +327,19 @@ public class TradeDetailsWindow extends Overlay { textArea.setText(trade.getContractAsJson()); String data = "Contract as json:\n"; data += trade.getContractAsJson(); - if (CurrencyUtil.isFiatCurrency(offer.getCurrencyCode())) { + data += "\n\nOther detail data:"; + if (offer.isFiatOffer()) { data += "\n\nBuyersAccountAge: " + buyersAccountAge; data += "\nSellersAccountAge: " + sellersAccountAge; } // TODO (woodser): include maker and taker deposit tx hex in contract? -// if (depositTx != null) { -// String depositTxAsHex = Utils.HEX.encode(depositTx.bitcoinSerialize(true)); -// data += "\n\nRaw deposit transaction as hex:\n" + depositTxAsHex; -// } +// if (depositTx != null) { +// String depositTxAsHex = Utils.HEX.encode(depositTx.bitcoinSerialize(true)); +// data += "\n\nRaw deposit transaction as hex:\n" + depositTxAsHex; +// } - data += "\n\nSelected arbitrator: " + DisputeAgentLookupMap.getKeyBaseUserName(contract.getArbitratorNodeAddress().getFullAddress()); + data += "\n\nSelected arbitrator: " + DisputeAgentLookupMap.getMatrixUserName(contract.getArbitratorNodeAddress().getFullAddress()); textArea.setText(data); textArea.setPrefHeight(50); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java index f6b6d08b73..6838974bb0 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java @@ -18,9 +18,9 @@ package bisq.desktop.main.overlays.windows; import bisq.desktop.main.overlays.Overlay; -import bisq.core.support.dispute.DisputeSummaryVerification; import bisq.core.locale.Res; +import bisq.core.support.dispute.DisputeSummaryVerification; import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; @@ -53,6 +53,7 @@ public class VerifyDisputeResultSignatureWindow extends Overlay list = FXCollections.observableArrayList(); + final AccountAgeWitnessService accountAgeWitnessService; + private final ObservableList list = FXCollections.observableArrayList(); private final ListChangeListener tradesListChangeListener; @Inject public ClosedTradesDataModel(ClosedTradableManager closedTradableManager, + ClosedTradableFormatter closedTradableFormatter, Preferences preferences, - TradeStatisticsManager tradeStatisticsManager, - PriceFeedService priceFeedService) { + PriceFeedService priceFeedService, + AccountAgeWitnessService accountAgeWitnessService) { this.closedTradableManager = closedTradableManager; + this.closedTradableFormatter = closedTradableFormatter; this.preferences = preferences; - this.tradeStatisticsManager = tradeStatisticsManager; this.priceFeedService = priceFeedService; + this.accountAgeWitnessService = accountAgeWitnessService; tradesListChangeListener = change -> applyList(); } @@ -83,57 +80,23 @@ class ClosedTradesDataModel extends ActivatableDataModel { closedTradableManager.getObservableList().removeListener(tradesListChangeListener); } - public ObservableList getList() { + ObservableList getList() { return list; } - public OfferPayload.Direction getDirection(Offer offer) { - return closedTradableManager.wasMyOffer(offer) ? offer.getDirection() : offer.getMirroredDirection(); - } - - private void applyList() { - list.clear(); - - list.addAll(closedTradableManager.getObservableList().stream().map(ClosedTradableListItem::new).collect(Collectors.toList())); - - // we sort by date, earliest first - list.sort((o1, o2) -> o2.getTradable().getDate().compareTo(o1.getTradable().getDate())); - } - - boolean wasMyOffer(Tradable tradable) { - return closedTradableManager.wasMyOffer(tradable.getOffer()); + List getListAsTradables() { + return list.stream().map(ClosedTradesListItem::getTradable).collect(Collectors.toList()); } Coin getTotalAmount() { - return Coin.valueOf(getList().stream() - .map(ClosedTradableListItem::getTradable) - .filter(e -> e instanceof Trade) - .map(e -> (Trade) e) - .mapToLong(Trade::getTradeAmountAsLong) - .sum()); + return ClosedTradableUtil.getTotalAmount(getListAsTradables()); } - Map getTotalVolumeByCurrency() { - Map map = new HashMap<>(); - getList().stream() - .map(ClosedTradableListItem::getTradable) - .filter(e -> e instanceof Trade) - .map(e -> (Trade) e) - .map(Trade::getTradeVolume) - .filter(Objects::nonNull) - .forEach(volume -> { - String currencyCode = volume.getCurrencyCode(); - map.putIfAbsent(currencyCode, 0L); - map.put(currencyCode, volume.getValue() + map.get(currencyCode)); - }); - return map; - } - - public Optional getVolumeInUserFiatCurrency(Coin amount) { + Optional getVolumeInUserFiatCurrency(Coin amount) { return getVolume(amount, preferences.getPreferredTradeCurrency().getCode()); } - public Optional getVolume(Coin amount, String currencyCode) { + Optional getVolume(Coin amount, String currencyCode) { MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); if (marketPrice == null) { return Optional.empty(); @@ -143,44 +106,30 @@ class ClosedTradesDataModel extends ActivatableDataModel { return Optional.of(VolumeUtil.getVolume(amount, price)); } - public Coin getTotalTxFee() { - return Coin.valueOf(getList().stream() - .map(ClosedTradableListItem::getTradable) - .mapToLong(tradable -> { - if (wasMyOffer(tradable) || tradable instanceof OpenOffer) { - return tradable.getOffer().getTxFee().value; - } else { - // taker pays for 3 transactions - return ((Trade) tradable).getTxFee().multiply(3).value; - } - }) - .sum()); + Volume getBsqVolumeInUsdWithAveragePrice(Coin amount) { + return closedTradableManager.getBsqVolumeInUsdWithAveragePrice(amount); } - public Coin getTotalTradeFee(boolean expectBtcFee) { - return Coin.valueOf(getList().stream() - .map(ClosedTradableListItem::getTradable) - .mapToLong(tradable -> getTradeFee(tradable, expectBtcFee)) - .sum()); + Coin getTotalTxFee() { + return ClosedTradableUtil.getTotalTxFee(getListAsTradables()); } - protected long getTradeFee(Tradable tradable, boolean expectBtcFee) { - Offer offer = tradable.getOffer(); - if (wasMyOffer(tradable) || tradable instanceof OpenOffer) { - String makerFeeTxId = offer.getOfferFeePaymentTxId(); - if (expectBtcFee) { - return offer.getMakerFee().value; - } else { - return 0; - } - } else { - Trade trade = (Trade) tradable; - String takerFeeTxId = trade.getTakerFeeTxId(); - if (expectBtcFee) { - return trade.getTakerFee().value; - } else { - return 0; - } - } + Coin getTotalTradeFee() { + return closedTradableManager.getTotalTradeFee(getListAsTradables()); + } + + boolean isCurrencyForTradeFeeBtc(Tradable item) { + return item != null; + } + + private void applyList() { + list.clear(); + list.addAll( + closedTradableManager.getObservableList().stream() + .map(tradable -> new ClosedTradesListItem(tradable, closedTradableFormatter, closedTradableManager)) + .collect(Collectors.toList()) + ); + // We sort by date, the earliest first + list.sort((o1, o2) -> o2.getTradable().getDate().compareTo(o1.getTradable().getDate())); } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesListItem.java b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesListItem.java new file mode 100644 index 0000000000..16a09985ba --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesListItem.java @@ -0,0 +1,179 @@ +/* + * 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 . + */ + +package bisq.desktop.main.portfolio.closedtrades; + +import bisq.desktop.util.DisplayUtils; +import bisq.desktop.util.filtering.FilterableListItem; +import bisq.desktop.util.filtering.FilteringUtils; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.monetary.Price; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; +import bisq.core.trade.ClosedTradableFormatter; +import bisq.core.trade.ClosedTradableManager; +import bisq.core.trade.Tradable; +import bisq.core.trade.Trade; +import org.bitcoinj.core.Coin; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Date; + +import lombok.Getter; + +public class ClosedTradesListItem implements FilterableListItem { + @Getter + private final Tradable tradable; + private final ClosedTradableFormatter closedTradableFormatter; + private final ClosedTradableManager closedTradableManager; + + public ClosedTradesListItem( + Tradable tradable, + ClosedTradableFormatter closedTradableFormatter, + ClosedTradableManager closedTradableManager) { + + this.tradable = tradable; + this.closedTradableFormatter = closedTradableFormatter; + this.closedTradableManager = closedTradableManager; + } + + public String getTradeId() { + return tradable.getShortId(); + } + + public Coin getAmount() { + return tradable.getOptionalAmount().orElse(null); + } + + public String getAmountAsString() { + return closedTradableFormatter.getAmountAsString(tradable); + } + + public Price getPrice() { + return tradable.getOptionalPrice().orElse(null); + } + + public String getPriceAsString() { + return closedTradableFormatter.getPriceAsString(tradable); + } + + public String getPriceDeviationAsString() { + return closedTradableFormatter.getPriceDeviationAsString(tradable); + } + + public String getVolumeAsString(boolean appendCode) { + return closedTradableFormatter.getVolumeAsString(tradable, appendCode); + } + + public String getVolumeCurrencyAsString() { + return closedTradableFormatter.getVolumeCurrencyAsString(tradable); + } + + public String getTxFeeAsString() { + return closedTradableFormatter.getTxFeeAsString(tradable); + } + + public String getTradeFeeAsString(boolean appendCode) { + return closedTradableFormatter.getTradeFeeAsString(tradable, appendCode); + } + + public String getBuyerSecurityDepositAsString() { + return closedTradableFormatter.getBuyerSecurityDepositAsString(tradable); + } + + public String getSellerSecurityDepositAsString() { + return closedTradableFormatter.getSellerSecurityDepositAsString(tradable); + } + + public String getDirectionLabel() { + Offer offer = tradable.getOffer(); + OfferDirection direction = closedTradableManager.wasMyOffer(offer) + ? offer.getDirection() + : offer.getMirroredDirection(); + String currencyCode = tradable.getOffer().getCurrencyCode(); + return DisplayUtils.getDirectionWithCode(direction, currencyCode); + } + + public Date getDate() { + return tradable.getDate(); + } + + public String getDateAsString() { + return DisplayUtils.formatDateTime(tradable.getDate()); + } + + public String getMarketLabel() { + return CurrencyUtil.getCurrencyPair(tradable.getOffer().getCurrencyCode()); + } + + public String getState() { + return closedTradableFormatter.getStateAsString(tradable); + } + + public int getNumPastTrades() { + return closedTradableManager.getNumPastTrades(tradable); + } + + @Override + public boolean match(String filterString) { + if (filterString.isEmpty()) { + return true; + } + if (StringUtils.containsIgnoreCase(getDateAsString(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getMarketLabel(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getPriceAsString(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getPriceDeviationAsString(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getVolumeAsString(true), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getAmountAsString(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getTradeFeeAsString(true), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getTxFeeAsString(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getBuyerSecurityDepositAsString(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getSellerSecurityDepositAsString(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getState(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getDirectionLabel(), filterString)) { + return true; + } + if (FilteringUtils.match(getTradable().getOffer(), filterString)) { + return true; + } + return getTradable() instanceof Trade && FilteringUtils.match((Trade) getTradable(), filterString); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml index 0943246960..49197fb6b9 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml @@ -18,28 +18,21 @@ --> - - - + - - - - - - + @@ -55,6 +48,7 @@ + diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java index 6ef6f7e602..d36beb5ee3 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java @@ -24,15 +24,13 @@ import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.AutoTooltipTableColumn; import bisq.desktop.components.HyperlinkWithIcon; -import bisq.desktop.components.InputTextField; -import bisq.desktop.components.PeerInfoIcon; -import bisq.desktop.main.MainView; +import bisq.desktop.components.PeerInfoIconTrading; +import bisq.desktop.components.list.FilterBox; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.ClosedTradesSummaryWindow; import bisq.desktop.main.overlays.windows.OfferDetailsWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; -import bisq.desktop.main.portfolio.PortfolioView; -import bisq.desktop.main.portfolio.duplicateoffer.DuplicateOfferView; +import bisq.desktop.main.portfolio.presentation.PortfolioUtil; import bisq.desktop.util.GUIUtil; import bisq.core.alert.PrivateNotificationManager; @@ -40,7 +38,6 @@ import bisq.core.locale.Res; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.offer.OpenOffer; -import bisq.core.trade.Contract; import bisq.core.trade.Tradable; import bisq.core.trade.Trade; import bisq.core.user.Preferences; @@ -54,12 +51,14 @@ import com.googlecode.jcsv.writer.CSVEntryConverter; import javax.inject.Inject; import javax.inject.Named; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import javafx.fxml.FXML; import javafx.stage.Stage; import javafx.scene.Node; +import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; @@ -69,7 +68,6 @@ import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; @@ -80,6 +78,8 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; +import javafx.event.ActionEvent; + import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; @@ -88,6 +88,8 @@ import javafx.util.Callback; import java.util.Comparator; import java.util.function.Function; +import static bisq.desktop.util.FormBuilder.getRegularIconButton; + @FxmlView public class ClosedTradesView extends ActivatableViewAndModel { private final boolean useDevPrivilegeKeys; @@ -103,6 +105,7 @@ public class ClosedTradesView extends ActivatableViewAndModel tableView; + TableView tableView; @FXML - TableColumn priceColumn, deviationColumn, amountColumn, volumeColumn, + TableColumn priceColumn, deviationColumn, amountColumn, volumeColumn, txFeeColumn, tradeFeeColumn, buyerSecurityDepositColumn, sellerSecurityDepositColumn, - marketColumn, directionColumn, dateColumn, tradeIdColumn, stateColumn, avatarColumn; + marketColumn, directionColumn, dateColumn, tradeIdColumn, stateColumn, + duplicateColumn, avatarColumn; @FXML - HBox searchBox; - @FXML - AutoTooltipLabel filterLabel; - @FXML - InputTextField filterTextField; - @FXML - Pane searchBoxSpacer; + FilterBox filterBox; @FXML AutoTooltipButton exportButton, summaryButton; @FXML @@ -147,9 +145,8 @@ public class ClosedTradesView extends ActivatableViewAndModel sortedList; - private FilteredList filteredList; - private ChangeListener filterTextFieldListener; + private SortedList sortedList; + private FilteredList filteredList; private ChangeListener widthListener; @Inject @@ -188,6 +185,7 @@ public class ClosedTradesView extends ActivatableViewAndModel o.getTradable().getId())); - dateColumn.setComparator(Comparator.comparing(o -> o.getTradable().getDate())); + tradeIdColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getTradeId)); + dateColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getDate)); directionColumn.setComparator(Comparator.comparing(o -> o.getTradable().getOffer().getDirection())); - marketColumn.setComparator(Comparator.comparing(model::getMarketLabel)); - priceColumn.setComparator(Comparator.comparing(model::getPrice, Comparator.nullsFirst(Comparator.naturalOrder()))); + marketColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getMarketLabel)); + priceColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getPrice, Comparator.nullsFirst(Comparator.naturalOrder()))); deviationColumn.setComparator(Comparator.comparing(o -> - o.getTradable().getOffer().isUseMarketBasedPrice() ? o.getTradable().getOffer().getMarketPriceMargin() : 1, + o.getTradable().getOffer().isUseMarketBasedPrice() ? o.getTradable().getOffer().getMarketPriceMarginPct() : 1, Comparator.nullsFirst(Comparator.naturalOrder()))); - volumeColumn.setComparator(nullsFirstComparingAsTrade(Trade::getTradeVolume)); - amountColumn.setComparator(Comparator.comparing(model::getAmount, Comparator.nullsFirst(Comparator.naturalOrder()))); - avatarColumn.setComparator(Comparator.comparing( - o -> model.getNumPastTrades(o.getTradable()), - Comparator.nullsFirst(Comparator.naturalOrder()) - )); + volumeColumn.setComparator(nullsFirstComparingAsTrade(Trade::getVolume)); + amountColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getAmount, Comparator.nullsFirst(Comparator.naturalOrder()))); + avatarColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getNumPastTrades, Comparator.nullsFirst(Comparator.naturalOrder()))); txFeeColumn.setComparator(nullsFirstComparing(o -> - o instanceof Trade ? ((Trade) o).getTxFee() : o.getOffer().getTxFee() + o.getTradable() instanceof Trade ? ((Trade) o.getTradable()).getTxFee() : o.getTradable().getOffer().getTxFee() )); - txFeeColumn.setComparator(Comparator.comparing(model::getTxFee, Comparator.nullsFirst(Comparator.naturalOrder()))); + txFeeColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getTxFeeAsString, Comparator.nullsFirst(Comparator.naturalOrder()))); // tradeFeeColumn.setComparator(Comparator.comparing(item -> { - String tradeFee = model.getTradeFee(item, true); - // We want to separate BTC fees so we use a prefix + String tradeFee = item.getTradeFeeAsString(true); return "BTC" + tradeFee; }, Comparator.nullsFirst(Comparator.naturalOrder()))); buyerSecurityDepositColumn.setComparator(nullsFirstComparing(o -> - o.getOffer() != null ? o.getOffer().getBuyerSecurityDeposit() : null + o.getTradable().getOffer() != null ? o.getTradable().getOffer().getBuyerSecurityDeposit() : null )); sellerSecurityDepositColumn.setComparator(nullsFirstComparing(o -> - o.getOffer() != null ? o.getOffer().getSellerSecurityDeposit() : null + o.getTradable().getOffer() != null ? o.getTradable().getOffer().getSellerSecurityDeposit() : null )); - stateColumn.setComparator(Comparator.comparing(model::getState)); + stateColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getState)); dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); tableView.setRowFactory( tableView -> { - final TableRow row = new TableRow<>(); - final ContextMenu rowMenu = new ContextMenu(); - MenuItem editItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); - editItem.setOnAction((event) -> { - try { - OfferPayload offerPayload = row.getItem().getTradable().getOffer().getOfferPayload(); - if (offerPayload.getPubKeyRing().equals(keyRing.getPubKeyRing())) { - navigation.navigateToWithData(offerPayload, MainView.class, PortfolioView.class, DuplicateOfferView.class); - } else { - new Popup().warning(Res.get("portfolio.context.notYourOffer")).show(); - } - } catch (NullPointerException e) { - log.warn("Unable to get offerPayload - {}", e.toString()); - } - }); - rowMenu.getItems().add(editItem); + TableRow row = new TableRow<>(); + ContextMenu rowMenu = new ContextMenu(); + MenuItem duplicateItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); + duplicateItem.setOnAction((ActionEvent event) -> onDuplicateOffer(row.getItem().getTradable().getOffer())); + rowMenu.getItems().add(duplicateItem); row.contextMenuProperty().bind( Bindings.when(Bindings.isNotNull(row.itemProperty())) .then(rowMenu) @@ -269,12 +253,6 @@ public class ClosedTradesView extends ActivatableViewAndModel applyFilteredListPredicate(filterTextField.getText()); - searchBox.setSpacing(5); - HBox.setHgrow(searchBoxSpacer, Priority.ALWAYS); - numItems.setId("num-offers"); numItems.setPadding(new Insets(-5, 0, 0, 10)); HBox.setHgrow(footerSpacer, Priority.ALWAYS); @@ -285,49 +263,56 @@ public class ClosedTradesView extends ActivatableViewAndModel(model.getList()); + filteredList = new FilteredList<>(model.dataModel.getList()); sortedList = new SortedList<>(filteredList); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(sortedList); + filterBox.initialize(filteredList, tableView); // here because filteredList is instantiated here + filterBox.activate(); + numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { - CSVEntryConverter headerConverter = item -> { + CSVEntryConverter headerConverter = item -> { String[] columns = new String[ColumnNames.values().length]; for (ColumnNames m : ColumnNames.values()) { columns[m.ordinal()] = m.toString(); } return columns; }; - CSVEntryConverter contentConverter = item -> { + CSVEntryConverter contentConverter = item -> { String[] columns = new String[ColumnNames.values().length]; - columns[ColumnNames.TRADE_ID.ordinal()] = model.getTradeId(item); - columns[ColumnNames.DATE.ordinal()] = model.getDate(item); - columns[ColumnNames.MARKET.ordinal()] = model.getMarketLabel(item); - columns[ColumnNames.PRICE.ordinal()] = model.getPrice(item); - columns[ColumnNames.DEVIATION.ordinal()] = model.getPriceDeviation(item); - columns[ColumnNames.AMOUNT.ordinal()] = model.getAmount(item); - columns[ColumnNames.VOLUME.ordinal()] = model.getVolume(item, false); - columns[ColumnNames.VOLUME_CURRENCY.ordinal()] = model.getVolumeCurrency(item); - columns[ColumnNames.TX_FEE.ordinal()] = model.getTxFee(item); - columns[ColumnNames.TRADE_FEE_BTC.ordinal()] = model.getTradeFee(item, false); - columns[ColumnNames.BUYER_SEC.ordinal()] = model.getBuyerSecurityDeposit(item); - columns[ColumnNames.SELLER_SEC.ordinal()] = model.getSellerSecurityDeposit(item); - columns[ColumnNames.OFFER_TYPE.ordinal()] = model.getDirectionLabel(item); - columns[ColumnNames.STATUS.ordinal()] = model.getState(item); + columns[ColumnNames.TRADE_ID.ordinal()] = item.getTradeId(); + columns[ColumnNames.DATE.ordinal()] = item.getDateAsString(); + columns[ColumnNames.MARKET.ordinal()] = item.getMarketLabel(); + columns[ColumnNames.PRICE.ordinal()] = item.getPriceAsString(); + columns[ColumnNames.DEVIATION.ordinal()] = item.getPriceDeviationAsString(); + columns[ColumnNames.AMOUNT.ordinal()] = item.getAmountAsString(); + columns[ColumnNames.VOLUME.ordinal()] = item.getVolumeAsString(false); + columns[ColumnNames.VOLUME_CURRENCY.ordinal()] = item.getVolumeCurrencyAsString(); + columns[ColumnNames.TX_FEE.ordinal()] = item.getTxFeeAsString(); + if (model.dataModel.isCurrencyForTradeFeeBtc(item.getTradable())) { + columns[ColumnNames.TRADE_FEE_BTC.ordinal()] = item.getTradeFeeAsString(false); + columns[ColumnNames.TRADE_FEE_BSQ.ordinal()] = ""; + } else { + columns[ColumnNames.TRADE_FEE_BTC.ordinal()] = ""; + columns[ColumnNames.TRADE_FEE_BSQ.ordinal()] = item.getTradeFeeAsString(false); + } + columns[ColumnNames.BUYER_SEC.ordinal()] = item.getBuyerSecurityDepositAsString(); + columns[ColumnNames.SELLER_SEC.ordinal()] = item.getSellerSecurityDepositAsString(); + columns[ColumnNames.OFFER_TYPE.ordinal()] = item.getDirectionLabel(); + columns[ColumnNames.STATUS.ordinal()] = item.getState(); return columns; }; GUIUtil.exportCSV("tradeHistory.csv", headerConverter, contentConverter, - new ClosedTradableListItem(null), sortedList, (Stage) root.getScene().getWindow()); + null, sortedList, (Stage) root.getScene().getWindow()); }); summaryButton.setOnAction(event -> new ClosedTradesSummaryWindow(model).show()); - filterTextField.textProperty().addListener(filterTextFieldListener); - applyFilteredListPredicate(filterTextField.getText()); root.widthProperty().addListener(widthListener); onWidthChange(root.getWidth()); } @@ -338,18 +323,18 @@ public class ClosedTradesView extends ActivatableViewAndModel> Comparator nullsFirstComparing(Function keyExtractor) { + private static > Comparator nullsFirstComparing(Function keyExtractor) { return Comparator.comparing( - o -> o.getTradable() != null ? keyExtractor.apply(o.getTradable()) : null, + o -> o != null ? keyExtractor.apply(o) : null, Comparator.nullsFirst(Comparator.naturalOrder()) ); } - private static > Comparator nullsFirstComparingAsTrade(Function keyExtractor) { + private static > Comparator nullsFirstComparingAsTrade(Function keyExtractor) { return Comparator.comparing( o -> o.getTradable() instanceof Trade ? keyExtractor.apply((Trade) o.getTradable()) : null, Comparator.nullsFirst(Comparator.naturalOrder()) @@ -363,94 +348,6 @@ public class ClosedTradesView extends ActivatableViewAndModel 1500); } - private void applyFilteredListPredicate(String filterString) { - filteredList.setPredicate(item -> { - if (filterString.isEmpty()) - return true; - - Tradable tradable = item.getTradable(); - Offer offer = tradable.getOffer(); - if (offer.getId().contains(filterString)) { - return true; - } - if (model.getDate(item).contains(filterString)) { - return true; - } - if (model.getMarketLabel(item).contains(filterString)) { - return true; - } - if (model.getPrice(item).contains(filterString)) { - return true; - } - if (model.getPriceDeviation(item).contains(filterString)) { - return true; - } - - if (model.getVolume(item, true).contains(filterString)) { - return true; - } - if (model.getAmount(item).contains(filterString)) { - return true; - } - if (model.getTradeFee(item, true).contains(filterString)) { - return true; - } - if (model.getTxFee(item).contains(filterString)) { - return true; - } - if (model.getBuyerSecurityDeposit(item).contains(filterString)) { - return true; - } - if (model.getSellerSecurityDeposit(item).contains(filterString)) { - return true; - } - if (model.getState(item).contains(filterString)) { - return true; - } - if (model.getDirectionLabel(item).contains(filterString)) { - return true; - } - if (offer.getPaymentMethod().getDisplayString().contains(filterString)) { - return true; - } - if (offer.getOfferFeePaymentTxId().contains(filterString)) { - return true; - } - - if (tradable instanceof Trade) { - Trade trade = (Trade) tradable; - if (trade.getTakerFeeTxId() != null && trade.getTakerFeeTxId().contains(filterString)) { - return true; - } - if (trade.getMaker().getDepositTxHash() != null && trade.getMaker().getDepositTxHash().contains(filterString)) { - return true; - } - if (trade.getTaker().getDepositTxHash() != null && trade.getTaker().getDepositTxHash().contains(filterString)) { - return true; - } - if (trade.getPayoutTxId() != null && trade.getPayoutTxId().contains(filterString)) { - return true; - } - - Contract contract = trade.getContract(); - boolean isBuyerOnion = false; - boolean isSellerOnion = false; - boolean matchesBuyersPaymentAccountData = false; - boolean matchesSellersPaymentAccountData = false; - if (contract != null) { - isBuyerOnion = contract.getBuyerNodeAddress().getFullAddress().contains(filterString); - isSellerOnion = contract.getSellerNodeAddress().getFullAddress().contains(filterString); - matchesBuyersPaymentAccountData = trade.getBuyer().getPaymentAccountPayload().getPaymentDetails().contains(filterString); - matchesSellersPaymentAccountData = trade.getSeller().getPaymentAccountPayload().getPaymentDetails().contains(filterString); - } - return isBuyerOnion || isSellerOnion || - matchesBuyersPaymentAccountData || matchesSellersPaymentAccountData; - } else { - return false; - } - }); - } - private void setTradeIdColumnCellFactory() { tradeIdColumn.getStyleClass().add("first-column"); tradeIdColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); @@ -458,22 +355,23 @@ public class ClosedTradesView extends ActivatableViewAndModel() { @Override - public TableCell call(TableColumn column) { + public TableCell call(TableColumn column) { return new TableCell<>() { private HyperlinkWithIcon field; @Override - public void updateItem(final ClosedTradableListItem item, boolean empty) { + public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { - field = new HyperlinkWithIcon(model.getTradeId(item)); + field = new HyperlinkWithIcon(item.getTradeId()); field.setOnAction(event -> { Tradable tradable = item.getTradable(); - if (tradable instanceof Trade) + if (tradable instanceof Trade) { tradeDetailsWindow.show((Trade) tradable); - else if (tradable instanceof OpenOffer) + } else if (tradable instanceof OpenOffer) { offerDetailsWindow.show(tradable.getOffer()); + } }); field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); setGraphic(field); @@ -489,18 +387,18 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + dateColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); dateColumn.setCellFactory( new Callback<>() { @Override - public TableCell call( - TableColumn column) { + public TableCell call( + TableColumn column) { return new TableCell<>() { @Override - public void updateItem(final ClosedTradableListItem item, boolean empty) { + public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) - setGraphic(new AutoTooltipLabel(model.getDate(item))); + setGraphic(new AutoTooltipLabel(item.getDateAsString())); else setGraphic(null); } @@ -510,17 +408,21 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + marketColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); marketColumn.setCellFactory( new Callback<>() { @Override - public TableCell call( - TableColumn column) { + public TableCell call( + TableColumn column) { return new TableCell<>() { @Override - public void updateItem(final ClosedTradableListItem item, boolean empty) { + public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); - setGraphic(new AutoTooltipLabel(model.getMarketLabel(item))); + if (item != null && !empty) { + setGraphic(new AutoTooltipLabel(item.getMarketLabel())); + } else { + setGraphic(null); + } } }; } @@ -528,18 +430,18 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + stateColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); stateColumn.setCellFactory( new Callback<>() { @Override - public TableCell call( - TableColumn column) { + public TableCell call( + TableColumn column) { return new TableCell<>() { @Override - public void updateItem(final ClosedTradableListItem item, boolean empty) { + public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) - setGraphic(new AutoTooltipLabel(model.getState(item))); + setGraphic(new AutoTooltipLabel(item.getState())); else setGraphic(null); } @@ -548,32 +450,66 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offerListItem.getValue())); + duplicateColumn.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + Button button; + + @Override + public void updateItem(final ClosedTradesListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty && isMyOfferAsMaker(item.getTradable().getOffer().getOfferPayload())) { + if (button == null) { + button = getRegularIconButton(MaterialDesignIcon.CONTENT_COPY); + button.setTooltip(new Tooltip(Res.get("shared.duplicateOffer"))); + setGraphic(button); + } + button.setOnAction(event -> onDuplicateOffer(item.getTradable().getOffer())); + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + } + @SuppressWarnings("UnusedReturnValue") - private TableColumn setAvatarColumnCellFactory() { + private TableColumn setAvatarColumnCellFactory() { avatarColumn.getStyleClass().addAll("last-column", "avatar-column"); - avatarColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); + avatarColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); avatarColumn.setCellFactory( new Callback<>() { @Override - public TableCell call(TableColumn column) { + public TableCell call(TableColumn column) { return new TableCell<>() { @Override - public void updateItem(final ClosedTradableListItem newItem, boolean empty) { - super.updateItem(newItem, empty); + public void updateItem(final ClosedTradesListItem item, boolean empty) { + super.updateItem(item, empty); - if (newItem != null && !empty && newItem.getTradable() instanceof Trade) { - Trade trade = (Trade) newItem.getTradable(); - int numPastTrades = model.getNumPastTrades(trade); - final NodeAddress tradingPeerNodeAddress = trade.getTradingPeerNodeAddress(); + if (!empty && item != null && item.getTradable() instanceof Trade) { + Trade tradeModel = (Trade) item.getTradable(); + int numPastTrades = item.getNumPastTrades(); + NodeAddress tradingPeerNodeAddress = tradeModel.getTradingPeerNodeAddress(); String role = Res.get("peerInfoIcon.tooltip.tradePeer"); - Node peerInfoIcon = new PeerInfoIcon(tradingPeerNodeAddress, + Node peerInfoIcon = new PeerInfoIconTrading(tradingPeerNodeAddress, role, numPastTrades, privateNotificationManager, - trade, + tradeModel, preferences, - model.accountAgeWitnessService, + model.dataModel.accountAgeWitnessService, useDevPrivilegeKeys); setPadding(new Insets(1, 15, 0, 0)); setGraphic(peerInfoIcon); @@ -588,17 +524,21 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + amountColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); amountColumn.setCellFactory( new Callback<>() { @Override - public TableCell call( - TableColumn column) { + public TableCell call( + TableColumn column) { return new TableCell<>() { @Override - public void updateItem(final ClosedTradableListItem item, boolean empty) { + public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); - setGraphic(new AutoTooltipLabel(model.getAmount(item))); + if (item != null && !empty) { + setGraphic(new AutoTooltipLabel(item.getAmountAsString())); + } else { + setGraphic(null); + } } }; } @@ -606,17 +546,21 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + priceColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); priceColumn.setCellFactory( new Callback<>() { @Override - public TableCell call( - TableColumn column) { + public TableCell call( + TableColumn column) { return new TableCell<>() { @Override - public void updateItem(final ClosedTradableListItem item, boolean empty) { + public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); - setGraphic(new AutoTooltipLabel(model.getPrice(item))); + if (item != null && !empty) { + setGraphic(new AutoTooltipLabel(item.getPriceAsString())); + } else { + setGraphic(null); + } } }; } @@ -624,17 +568,21 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + deviationColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); deviationColumn.setCellFactory( new Callback<>() { @Override - public TableCell call( - TableColumn column) { + public TableCell call( + TableColumn column) { return new TableCell<>() { @Override - public void updateItem(final ClosedTradableListItem item, boolean empty) { + public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); - setGraphic(new AutoTooltipLabel(model.getPriceDeviation(item))); + if (item != null && !empty) { + setGraphic(new AutoTooltipLabel(item.getPriceDeviationAsString())); + } else { + setGraphic(null); + } } }; } @@ -642,18 +590,18 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + volumeColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); volumeColumn.setCellFactory( new Callback<>() { @Override - public TableCell call( - TableColumn column) { + public TableCell call( + TableColumn column) { return new TableCell<>() { @Override - public void updateItem(final ClosedTradableListItem item, boolean empty) { + public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) - setGraphic(new AutoTooltipLabel(model.getVolume(item, true))); + setGraphic(new AutoTooltipLabel(item.getVolumeAsString(true))); else setGraphic(null); } @@ -663,17 +611,21 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + directionColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); directionColumn.setCellFactory( new Callback<>() { @Override - public TableCell call( - TableColumn column) { + public TableCell call( + TableColumn column) { return new TableCell<>() { @Override - public void updateItem(final ClosedTradableListItem item, boolean empty) { + public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); - setGraphic(new AutoTooltipLabel(model.getDirectionLabel(item))); + if (item != null && !empty) { + setGraphic(new AutoTooltipLabel(item.getDirectionLabel())); + } else { + setGraphic(null); + } } }; } @@ -681,17 +633,21 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + txFeeColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); txFeeColumn.setCellFactory( new Callback<>() { @Override - public TableCell call( - TableColumn column) { + public TableCell call( + TableColumn column) { return new TableCell<>() { @Override - public void updateItem(final ClosedTradableListItem item, boolean empty) { + public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); - setGraphic(new AutoTooltipLabel(model.getTxFee(item))); + if (item != null && !empty) { + setGraphic(new AutoTooltipLabel(item.getTxFeeAsString())); + } else { + setGraphic(null); + } } }; } @@ -699,17 +655,21 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + tradeFeeColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); tradeFeeColumn.setCellFactory( new Callback<>() { @Override - public TableCell call( - TableColumn column) { + public TableCell call( + TableColumn column) { return new TableCell<>() { @Override - public void updateItem(final ClosedTradableListItem item, boolean empty) { + public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); - setGraphic(new AutoTooltipLabel(model.getTradeFee(item, true))); + if (item != null && !empty) { + setGraphic(new AutoTooltipLabel(item.getTradeFeeAsString(true))); + } else { + setGraphic(null); + } } }; } @@ -717,17 +677,21 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + buyerSecurityDepositColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); buyerSecurityDepositColumn.setCellFactory( new Callback<>() { @Override - public TableCell call( - TableColumn column) { + public TableCell call( + TableColumn column) { return new TableCell<>() { @Override - public void updateItem(final ClosedTradableListItem item, boolean empty) { + public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); - setGraphic(new AutoTooltipLabel(model.getBuyerSecurityDeposit(item))); + if (item != null && !empty) { + setGraphic(new AutoTooltipLabel(item.getBuyerSecurityDepositAsString())); + } else { + setGraphic(null); + } } }; } @@ -735,21 +699,41 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + sellerSecurityDepositColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); sellerSecurityDepositColumn.setCellFactory( new Callback<>() { @Override - public TableCell call( - TableColumn column) { + public TableCell call( + TableColumn column) { return new TableCell<>() { @Override - public void updateItem(final ClosedTradableListItem item, boolean empty) { + public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); - setGraphic(new AutoTooltipLabel(model.getSellerSecurityDeposit(item))); + if (item != null && !empty) { + setGraphic(new AutoTooltipLabel(item.getSellerSecurityDepositAsString())); + } else { + setGraphic(null); + } } }; } }); } + private void onDuplicateOffer(Offer offer) { + try { + OfferPayload offerPayload = offer.getOfferPayload(); + if (isMyOfferAsMaker(offerPayload)) { + PortfolioUtil.duplicateOffer(navigation, offerPayload); + } else { + new Popup().warning(Res.get("portfolio.context.notYourOffer")).show(); + } + } catch (NullPointerException e) { + log.warn("Unable to get offerPayload - {}", e.toString()); + } + } + + private boolean isMyOfferAsMaker(OfferPayload offerPayload) { + return offerPayload.getPubKeyRing().equals(keyRing.getPubKeyRing()); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java index 7e4240da09..a94be340a7 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java @@ -19,234 +19,29 @@ package bisq.desktop.main.portfolio.closedtrades; import bisq.desktop.common.model.ActivatableWithDataModel; import bisq.desktop.common.model.ViewModel; -import bisq.desktop.util.DisplayUtils; -import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.locale.CurrencyUtil; -import bisq.core.locale.Res; -import bisq.core.monetary.Altcoin; import bisq.core.monetary.Volume; -import bisq.core.offer.Offer; -import bisq.core.offer.OpenOffer; -import bisq.core.trade.Tradable; -import bisq.core.trade.Trade; -import bisq.core.util.FormattingUtils; -import bisq.core.util.coin.CoinFormatter; - -import bisq.network.p2p.NodeAddress; +import bisq.core.trade.ClosedTradableFormatter; import org.bitcoinj.core.Coin; -import org.bitcoinj.core.Monetary; -import org.bitcoinj.utils.Fiat; import com.google.inject.Inject; -import javax.inject.Named; - -import javafx.collections.ObservableList; - import java.util.Map; -import java.util.stream.Collectors; public class ClosedTradesViewModel extends ActivatableWithDataModel implements ViewModel { - private final CoinFormatter btcFormatter; - final AccountAgeWitnessService accountAgeWitnessService; + private final ClosedTradableFormatter closedTradableFormatter; @Inject - public ClosedTradesViewModel(ClosedTradesDataModel dataModel, - AccountAgeWitnessService accountAgeWitnessService, - @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { + public ClosedTradesViewModel(ClosedTradesDataModel dataModel, ClosedTradableFormatter closedTradableFormatter) { super(dataModel); - this.accountAgeWitnessService = accountAgeWitnessService; - this.btcFormatter = btcFormatter; + + this.closedTradableFormatter = closedTradableFormatter; } - public ObservableList getList() { - return dataModel.getList(); - } - - String getTradeId(ClosedTradableListItem item) { - return item.getTradable().getShortId(); - } - - String getAmount(ClosedTradableListItem item) { - if (item != null && item.getTradable() instanceof Trade) - return btcFormatter.formatCoin(((Trade) item.getTradable()).getTradeAmount()); - else - return ""; - } - - String getPrice(ClosedTradableListItem item) { - if (item == null) - return ""; - Tradable tradable = item.getTradable(); - if (tradable instanceof Trade) - return FormattingUtils.formatPrice(((Trade) tradable).getTradePrice()); - else - return FormattingUtils.formatPrice(tradable.getOffer().getPrice()); - } - - String getPriceDeviation(ClosedTradableListItem item) { - if (item == null) - return ""; - Tradable tradable = item.getTradable(); - if (tradable.getOffer().isUseMarketBasedPrice()) { - return FormattingUtils.formatPercentagePrice(tradable.getOffer().getMarketPriceMargin()); - } else { - return Res.get("shared.na"); - } - } - - String getVolume(ClosedTradableListItem item, boolean appendCode) { - if (item == null) { - return ""; - } - - if (item.getTradable() instanceof OpenOffer) { - return ""; - } - - Trade trade = (Trade) item.getTradable(); - return DisplayUtils.formatVolume(trade.getTradeVolume(), appendCode); - } - - String getVolumeCurrency(ClosedTradableListItem item) { - if (item == null) { - return ""; - } - Volume volume; - if (item.getTradable() instanceof OpenOffer) { - OpenOffer openOffer = (OpenOffer) item.getTradable(); - volume = openOffer.getOffer().getVolume(); - } else { - Trade trade = (Trade) item.getTradable(); - volume = trade.getTradeVolume(); - } - return volume != null ? volume.getCurrencyCode() : ""; - } - - String getTxFee(ClosedTradableListItem item) { - if (item == null) - return ""; - Tradable tradable = item.getTradable(); - if (!wasMyOffer(tradable) && (tradable instanceof Trade)) { - // taker pays for 3 transactions - return btcFormatter.formatCoin(((Trade) tradable).getTxFee().multiply(3)); - } else { - return btcFormatter.formatCoin(tradable.getOffer().getTxFee()); - } - } - - - String getTradeFee(ClosedTradableListItem item, boolean appendCode) { - if (item == null) { - return ""; - } - - Tradable tradable = item.getTradable(); - Offer offer = tradable.getOffer(); - if (wasMyOffer(tradable) || tradable instanceof OpenOffer) { - return btcFormatter.formatCoin(offer.getMakerFee(), appendCode); - } else { - Trade trade = (Trade) tradable; - String takerFeeTxId = trade.getTakerFeeTxId(); - return btcFormatter.formatCoin(trade.getTakerFee(), appendCode); - } - } - - String getBuyerSecurityDeposit(ClosedTradableListItem item) { - if (item == null) - return ""; - Tradable tradable = item.getTradable(); - if (tradable.getOffer() != null) - return btcFormatter.formatCoin(tradable.getOffer().getBuyerSecurityDeposit()); - else - return ""; - } - - String getSellerSecurityDeposit(ClosedTradableListItem item) { - if (item == null) - return ""; - Tradable tradable = item.getTradable(); - if (tradable.getOffer() != null) - return btcFormatter.formatCoin(tradable.getOffer().getSellerSecurityDeposit()); - else - return ""; - } - - String getDirectionLabel(ClosedTradableListItem item) { - return (item != null) ? DisplayUtils.getDirectionWithCode(dataModel.getDirection(item.getTradable().getOffer()), item.getTradable().getOffer().getCurrencyCode()) : ""; - } - - String getDate(ClosedTradableListItem item) { - return DisplayUtils.formatDateTime(item.getTradable().getDate()); - } - - String getMarketLabel(ClosedTradableListItem item) { - if ((item == null)) - return ""; - - return CurrencyUtil.getCurrencyPair(item.getTradable().getOffer().getCurrencyCode()); - } - - String getState(ClosedTradableListItem item) { - if (item != null) { - if (item.getTradable() instanceof Trade) { - Trade trade = (Trade) item.getTradable(); - - if (trade.isWithdrawn() || trade.isPayoutPublished()) { - return Res.get("portfolio.closed.completed"); - } else if (trade.getDisputeState() == Trade.DisputeState.DISPUTE_CLOSED) { - return Res.get("portfolio.closed.ticketClosed"); - } else if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED) { - return Res.get("portfolio.closed.mediationTicketClosed"); - } else if (trade.getDisputeState() == Trade.DisputeState.REFUND_REQUEST_CLOSED) { - return Res.get("portfolio.closed.ticketClosed"); - } else { - log.error("That must not happen. We got a pending state but we are in the closed trades list. state={}", trade.getState().toString()); - return Res.get("shared.na"); - } - } else if (item.getTradable() instanceof OpenOffer) { - OpenOffer.State state = ((OpenOffer) item.getTradable()).getState(); - log.trace("OpenOffer state {}", state); - switch (state) { - case AVAILABLE: - case RESERVED: - case CLOSED: - log.error("Invalid state {}", state); - return state.toString(); - case CANCELED: - return Res.get("portfolio.closed.canceled"); - case DEACTIVATED: - log.error("Invalid state {}", state); - return state.toString(); - default: - log.error("Unhandled state {}", state); - return state.toString(); - } - } - } - return ""; - } - - int getNumPastTrades(Tradable tradable) { - return dataModel.closedTradableManager.getObservableList().stream() - .filter(candidate -> { - if (!(candidate instanceof Trade) || - !(tradable instanceof Trade)) return false; - NodeAddress candidateAddress = ((Trade) candidate).getTradingPeerNodeAddress(); - NodeAddress tradableAddress = ((Trade) tradable).getTradingPeerNodeAddress(); - return candidateAddress != null && - tradableAddress != null && - candidateAddress.getFullAddress().equals(tradableAddress.getFullAddress()); - }) - .collect(Collectors.toSet()) - .size(); - } - - boolean wasMyOffer(Tradable tradable) { - return dataModel.wasMyOffer(tradable); - } + /////////////////////////////////////////////////////////////////////////////////////////// + // Used in ClosedTradesSummaryWindow + /////////////////////////////////////////////////////////////////////////////////////////// public Coin getTotalTradeAmount() { return dataModel.getTotalAmount(); @@ -254,43 +49,21 @@ public class ClosedTradesViewModel extends ActivatableWithDataModel { - return Res.get("closedTradesSummaryWindow.totalAmount.value", - btcFormatter.formatCoin(totalTradeAmount, true), - DisplayUtils.formatVolumeWithCode(volume)); - }) + .map(volume -> closedTradableFormatter.getTotalAmountWithVolumeAsString(totalTradeAmount, volume)) .orElse(""); } public Map getTotalVolumeByCurrency() { - return dataModel.getTotalVolumeByCurrency().entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, - entry -> { - String currencyCode = entry.getKey(); - Monetary monetary; - if (CurrencyUtil.isCryptoCurrency(currencyCode)) { - monetary = Altcoin.valueOf(currencyCode, entry.getValue()); - } else { - monetary = Fiat.valueOf(currencyCode, entry.getValue()); - } - return DisplayUtils.formatVolumeWithCode(new Volume(monetary)); - } - )); + return closedTradableFormatter.getTotalVolumeByCurrencyAsString(dataModel.getListAsTradables()); } public String getTotalTxFee(Coin totalTradeAmount) { Coin totalTxFee = dataModel.getTotalTxFee(); - double percentage = ((double) totalTxFee.value) / totalTradeAmount.value; - return Res.get("closedTradesSummaryWindow.totalMinerFee.value", - btcFormatter.formatCoin(totalTxFee, true), - FormattingUtils.formatToPercentWithSymbol(percentage)); + return closedTradableFormatter.getTotalTxFeeAsString(totalTradeAmount, totalTxFee); } public String getTotalTradeFeeInBtc(Coin totalTradeAmount) { - Coin totalTradeFee = dataModel.getTotalTradeFee(true); - double percentage = ((double) totalTradeFee.value) / totalTradeAmount.value; - return Res.get("closedTradesSummaryWindow.totalTradeFeeInBtc.value", - btcFormatter.formatCoin(totalTradeFee, true), - FormattingUtils.formatToPercentWithSymbol(percentage)); + Coin totalTradeFee = dataModel.getTotalTradeFee(); + return closedTradableFormatter.getTotalTradeFeeInBtcAsString(totalTradeAmount, totalTradeFee); } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java index 91a29945f2..965b44100d 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java @@ -22,11 +22,15 @@ import bisq.desktop.Navigation; import bisq.desktop.main.offer.MutableOfferDataModel; import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.btc.wallet.Restrictions; import bisq.core.btc.wallet.XmrWalletService; -import bisq.core.offer.CreateOfferService; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.TradeCurrency; import bisq.core.offer.Offer; import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOfferManager; +import bisq.core.offer.CreateOfferService; +import bisq.core.payment.PaymentAccount; import bisq.core.provider.fee.FeeService; import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.statistics.TradeStatisticsManager; @@ -34,13 +38,21 @@ import bisq.core.user.Preferences; import bisq.core.user.User; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.coin.CoinUtil; import bisq.network.p2p.P2PService; +import org.bitcoinj.core.Coin; + import com.google.inject.Inject; import javax.inject.Named; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + class DuplicateOfferDataModel extends MutableOfferDataModel { @Inject @@ -82,8 +94,46 @@ class DuplicateOfferDataModel extends MutableOfferDataModel { setPrice(offer.getPrice()); setVolume(offer.getVolume()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); + + setBuyerSecurityDeposit(getBuyerSecurityAsPercent(offer)); + if (offer.isUseMarketBasedPrice()) { - setMarketPriceMargin(offer.getMarketPriceMargin()); + setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); } } + + private double getBuyerSecurityAsPercent(Offer offer) { + Coin offerBuyerSecurityDeposit = getBoundedBuyerSecurityDepositAsCoin(offer.getBuyerSecurityDeposit()); + double offerBuyerSecurityDepositAsPercent = CoinUtil.getAsPercentPerBtc(offerBuyerSecurityDeposit, + offer.getAmount()); + return Math.min(offerBuyerSecurityDepositAsPercent, + Restrictions.getMaxBuyerSecurityDepositAsPercent()); + } + + @Override + protected Set getUserPaymentAccounts() { + return Objects.requireNonNull(user.getPaymentAccounts()).stream() + .filter(account -> !account.getPaymentMethod().isBsqSwap()) + .collect(Collectors.toSet()); + } + + @Override + protected PaymentAccount getPreselectedPaymentAccount() { + // If trade currency is BSQ don't use the BSQ swap payment account as it will automatically + // close the duplicate offer view + Optional bsqOptional = CurrencyUtil.getTradeCurrency("BSQ"); + if (bsqOptional.isPresent() && tradeCurrency.equals(bsqOptional.get()) && user.getPaymentAccounts() != null) { + Optional firstBsqPaymentAccount = user.getPaymentAccounts().stream().filter(paymentAccount1 -> { + Optional tradeCurrency = paymentAccount1.getTradeCurrency(); + return tradeCurrency.isPresent() && + tradeCurrency.get().equals(bsqOptional.get()); + }).findFirst(); + + if (firstBsqPaymentAccount.isPresent()) { + return firstBsqPaymentAccount.get(); + } + } + + return super.getPreselectedPaymentAccount(); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/duplicateoffer/DuplicateOfferView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/duplicateoffer/DuplicateOfferView.java index aafb72e26d..bdb9e895e9 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/duplicateoffer/DuplicateOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/duplicateoffer/DuplicateOfferView.java @@ -24,12 +24,14 @@ import bisq.desktop.main.overlays.windows.OfferDetailsWindow; import bisq.core.locale.CurrencyUtil; import bisq.core.offer.OfferPayload; +import bisq.core.payment.PaymentAccount; import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; import com.google.inject.Inject; - +import javafx.collections.ObservableList; +import javafx.geometry.Insets; import javax.inject.Named; @FxmlView @@ -53,14 +55,24 @@ public class DuplicateOfferView extends MutableOfferView filterPaymentAccounts(ObservableList paymentAccounts) { + return paymentAccounts; + } + public void initWithData(OfferPayload offerPayload) { - initWithData(offerPayload.getDirection(), CurrencyUtil.getTradeCurrency(offerPayload.getCurrencyCode()).get()); + initWithData(offerPayload.getDirection(), + CurrencyUtil.getTradeCurrency(offerPayload.getCurrencyCode()).get(), + null); model.initWithData(offerPayload); } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java index 615eea3c15..4561a47e8d 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -28,6 +28,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.locale.TradeCurrency; import bisq.core.offer.CreateOfferService; import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOffer; @@ -53,6 +54,7 @@ import com.google.inject.Inject; import javax.inject.Named; import java.util.Optional; +import java.util.Set; class EditOfferDataModel extends MutableOfferDataModel { @@ -143,7 +145,7 @@ class EditOfferDataModel extends MutableOfferDataModel { } @Override - public boolean initWithData(OfferPayload.Direction direction, TradeCurrency tradeCurrency) { + public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) { try { return super.initWithData(direction, tradeCurrency); } catch (NullPointerException e) { @@ -169,7 +171,7 @@ class EditOfferDataModel extends MutableOfferDataModel { setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); setTriggerPrice(openOffer.getTriggerPrice()); if (offer.isUseMarketBasedPrice()) { - setMarketPriceMargin(offer.getMarketPriceMargin()); + setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); } } @@ -190,7 +192,7 @@ class EditOfferDataModel extends MutableOfferDataModel { offerPayload.getPubKeyRing(), offerPayload.getDirection(), newOfferPayload.getPrice(), - newOfferPayload.getMarketPriceMargin(), + newOfferPayload.getMarketPriceMarginPct(), newOfferPayload.isUseMarketBasedPrice(), offerPayload.getAmount(), offerPayload.getMinAmount(), @@ -238,4 +240,9 @@ class EditOfferDataModel extends MutableOfferDataModel { openOfferManager.editOpenOfferCancel(openOffer, initialState, () -> { }, errorMessageHandler); } + + @Override + protected Set getUserPaymentAccounts() { + throw new RuntimeException("Edit offer not supported with XMR"); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferView.java index eda4920923..496ae9b8f9 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferView.java @@ -28,6 +28,7 @@ import bisq.desktop.main.overlays.windows.OfferDetailsWindow; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.offer.OpenOffer; +import bisq.core.payment.PaymentAccount; import bisq.core.user.DontShowAgainLookup; import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; @@ -44,7 +45,7 @@ import javafx.scene.control.Label; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; - +import javafx.collections.ObservableList; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -113,7 +114,7 @@ public class EditOfferView extends MutableOfferView { model.isNextButtonDisabled.setValue(false); cancelButton.setDisable(false); - model.onInvalidateMarketPriceMargin(); + model.onInvalidateMarketPriceMarginPct(); model.onInvalidatePrice(); // To force re-validation of payment account validation @@ -143,7 +144,8 @@ public class EditOfferView extends MutableOfferView { model.applyOpenOffer(openOffer); initWithData(openOffer.getOffer().getDirection(), - CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()).get()); + CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()).get(), + null); model.onStartEditOffer(errorMessage -> { log.error(errorMessage); @@ -171,6 +173,11 @@ public class EditOfferView extends MutableOfferView { confirmButton.disableProperty().unbind(); } + @Override + protected ObservableList filterPaymentAccounts(ObservableList paymentAccounts) { + return paymentAccounts; + } + /////////////////////////////////////////////////////////////////////////////////////////// // Build UI elements /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java index da0f6c8a75..647ae59d51 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java @@ -103,9 +103,9 @@ class EditOfferViewModel extends MutableOfferViewModel { dataModel.onCancelEditOffer(errorMessageHandler); } - public void onInvalidateMarketPriceMargin() { + public void onInvalidateMarketPriceMarginPct() { marketPriceMargin.set("0.00%"); - marketPriceMargin.set(FormattingUtils.formatToPercent(dataModel.getMarketPriceMargin())); + marketPriceMargin.set(FormattingUtils.formatToPercent(dataModel.getMarketPriceMarginPct())); } public void onInvalidatePrice() { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java index 99ce7ed779..c0c24674b5 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java @@ -20,6 +20,7 @@ package bisq.desktop.main.portfolio.failedtrades; import bisq.desktop.common.model.ActivatableDataModel; import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferPayload; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; @@ -64,7 +65,7 @@ class FailedTradesDataModel extends ActivatableDataModel { return list; } - public OfferPayload.Direction getDirection(Offer offer) { + public OfferDirection getDirection(Offer offer) { return failedTradesManager.wasMyOffer(offer) ? offer.getDirection() : offer.getMirroredDirection(); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java index 19490b4887..de6b195e8a 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java @@ -144,9 +144,9 @@ public class FailedTradesView extends ActivatableViewAndModel o.getTrade().getId())); dateColumn.setComparator(Comparator.comparing(o -> o.getTrade().getDate())); - priceColumn.setComparator(Comparator.comparing(o -> o.getTrade().getTradePrice())); - volumeColumn.setComparator(Comparator.comparing(o -> o.getTrade().getTradeVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); - amountColumn.setComparator(Comparator.comparing(o -> o.getTrade().getTradeAmount(), Comparator.nullsFirst(Comparator.naturalOrder()))); + priceColumn.setComparator(Comparator.comparing(o -> o.getTrade().getPrice())); + volumeColumn.setComparator(Comparator.comparing(o -> o.getTrade().getVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); + amountColumn.setComparator(Comparator.comparing(o -> o.getTrade().getAmount(), Comparator.nullsFirst(Comparator.naturalOrder()))); stateColumn.setComparator(Comparator.comparing(model::getState)); marketColumn.setComparator(Comparator.comparing(model::getMarketLabel)); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesViewModel.java index 95a5687aad..052a0299a0 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesViewModel.java @@ -24,6 +24,7 @@ import bisq.desktop.util.DisplayUtils; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.util.FormattingUtils; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.CoinFormatter; import com.google.inject.Inject; @@ -53,18 +54,18 @@ class FailedTradesViewModel extends ActivatableWithDataModel } String getVolume(OpenOfferListItem item) { - return (item != null) ? DisplayUtils.formatVolume(item.getOffer(), false, 0) + " " + item.getOffer().getCurrencyCode() : ""; + return (item != null) ? VolumeUtil.formatVolume(item.getOffer(), false, 0) + " " + item.getOffer().getCurrencyCode() : ""; } String getDirectionLabel(OpenOfferListItem item) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/BuyerSubView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/BuyerSubView.java index d35a5bd340..8055ca5f4d 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/BuyerSubView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/BuyerSubView.java @@ -29,8 +29,6 @@ import org.fxmisc.easybind.EasyBind; import lombok.extern.slf4j.Slf4j; -import javafx.application.Platform; - @Slf4j public class BuyerSubView extends TradeSubView { private TradeWizardItem step1; @@ -76,44 +74,40 @@ public class BuyerSubView extends TradeSubView { protected void onViewStateChanged(PendingTradesViewModel.State viewState) { super.onViewStateChanged(viewState); - Platform.runLater(new Runnable() { - @Override public void run() { - if (viewState != null) { - PendingTradesViewModel.BuyerState buyerState = (PendingTradesViewModel.BuyerState) viewState; + if (viewState != null && model.getTrade() != null) { + PendingTradesViewModel.BuyerState buyerState = (PendingTradesViewModel.BuyerState) viewState; - step1.setDisabled(); - step2.setDisabled(); - step3.setDisabled(); - step4.setDisabled(); + step1.setDisabled(); + step2.setDisabled(); + step3.setDisabled(); + step4.setDisabled(); - switch (buyerState) { - case UNDEFINED: - break; - case STEP1: - showItem(step1); - break; - case STEP2: - step1.setCompleted(); - showItem(step2); - break; - case STEP3: - step1.setCompleted(); - step2.setCompleted(); - showItem(step3); - break; - case STEP4: - step1.setCompleted(); - step2.setCompleted(); - step3.setCompleted(); - showItem(step4); - break; - default: - log.warn("unhandled buyerState " + buyerState); - break; - } - } + switch (buyerState) { + case UNDEFINED: + break; + case STEP1: + showItem(step1); + break; + case STEP2: + step1.setCompleted(); + showItem(step2); + break; + case STEP3: + step1.setCompleted(); + step2.setCompleted(); + showItem(step3); + break; + case STEP4: + step1.setCompleted(); + step2.setCompleted(); + step3.setCompleted(); + showItem(step4); + break; + default: + log.warn("unhandled buyerState " + buyerState); + break; } - }); + } } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index 7d1cdc3675..e6142ff981 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -34,7 +34,7 @@ import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferUtil; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.support.SupportType; @@ -52,7 +52,8 @@ import bisq.core.trade.TradeManager; import bisq.core.trade.protocol.BuyerProtocol; import bisq.core.trade.protocol.SellerProtocol; import bisq.core.user.Preferences; - +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.CoinFormatter; import bisq.network.p2p.P2PService; import bisq.common.UserThread; import bisq.common.crypto.PubKeyRing; @@ -83,6 +84,7 @@ import java.util.stream.Collectors; import lombok.Getter; import javax.annotation.Nullable; +import javax.inject.Named; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @@ -105,6 +107,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { public final WalletPasswordWindow walletPasswordWindow; private final NotificationCenter notificationCenter; private final OfferUtil offerUtil; + private final CoinFormatter btcFormatter; final ObservableList list = FXCollections.observableArrayList(); private final ListChangeListener tradesListChangeListener; @@ -143,7 +146,8 @@ public class PendingTradesDataModel extends ActivatableDataModel { WalletPasswordWindow walletPasswordWindow, NotificationCenter notificationCenter, OfferUtil offerUtil, - CoreDisputesService disputesService) { + CoreDisputesService disputesService, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) { this.tradeManager = tradeManager; this.xmrWalletService = xmrWalletService; this.pubKeyRingProvider = pubKeyRingProvider; @@ -159,6 +163,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { this.notificationCenter = notificationCenter; this.offerUtil = offerUtil; this.disputesService = disputesService; + this.btcFormatter = formatter; tradesListChangeListener = change -> onListChanged(); notificationCenter.setSelectItemByTradeIdConsumer(this::selectItemByTradeId); @@ -192,7 +197,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Trade trade = getTrade(); checkNotNull(trade, "trade must not be null"); - checkArgument(trade instanceof BuyerTrade, "Check failed: trade instanceof BuyerTrade"); + checkArgument(trade instanceof BuyerTrade, "Check failed: trade instanceof BuyerTrade. Was: " + trade.getClass().getSimpleName()); ((BuyerProtocol) tradeManager.getTradeProtocol(trade)).onPaymentStarted(resultHandler, errorMessageHandler); } @@ -335,7 +340,9 @@ public class PendingTradesDataModel extends ActivatableDataModel { private void onListChanged() { list.clear(); - list.addAll(tradeManager.getObservableList().stream().map(PendingTradesListItem::new).collect(Collectors.toList())); + list.addAll(tradeManager.getObservableList().stream() + .map(trade -> new PendingTradesListItem(trade, btcFormatter)) + .collect(Collectors.toList())); // we sort by date, earliest first list.sort((o1, o2) -> o2.getTrade().getDate().compareTo(o1.getTrade().getDate())); @@ -492,7 +499,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { // trade.getId(), // pubKeyRing.hashCode(), // traderId // true, -// (offer.getDirection() == OfferPayload.Direction.BUY) == isMaker, +// (offer.getDirection() == OfferDirection.BUY) == isMaker, // isMaker, // pubKeyRing, // trade.getDate().getTime(), @@ -522,7 +529,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { trade.getId(), pubKeyRingProvider.get().hashCode(), // trader id true, - (offer.getDirection() == OfferPayload.Direction.BUY) == isMaker, + (offer.getDirection() == OfferDirection.BUY) == isMaker, isMaker, pubKeyRingProvider.get(), trade.getDate().getTime(), diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesListItem.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesListItem.java index 728547107a..103f42c1cc 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesListItem.java @@ -17,39 +17,74 @@ package bisq.desktop.main.portfolio.pendingtrades; +import bisq.desktop.util.filtering.FilterableListItem; + import bisq.core.monetary.Price; -import bisq.core.monetary.Volume; import bisq.core.trade.Trade; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.CoinFormatter; -import org.bitcoinj.core.Coin; +import org.apache.commons.lang3.StringUtils; -import javafx.beans.property.ReadOnlyObjectProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static bisq.core.locale.CurrencyUtil.getCurrencyPair; /** * We could remove that wrapper if it is not needed for additional UI only fields. */ -public class PendingTradesListItem { - +public class PendingTradesListItem implements FilterableListItem { + public static final Logger log = LoggerFactory.getLogger(PendingTradesListItem.class); + private final CoinFormatter btcFormatter; private final Trade trade; - public PendingTradesListItem(Trade trade) { + public PendingTradesListItem(Trade trade, CoinFormatter btcFormatter) { this.trade = trade; + this.btcFormatter = btcFormatter; } public Trade getTrade() { return trade; } - public ReadOnlyObjectProperty tradeAmountProperty() { - return trade.tradeAmountProperty(); - } - - public ReadOnlyObjectProperty tradeVolumeProperty() { - return trade.tradeVolumeProperty(); - } - public Price getPrice() { - return trade.getTradePrice(); + return trade.getPrice(); } + public String getPriceAsString() { + return FormattingUtils.formatPrice(trade.getPrice()); + } + + public String getAmountAsString() { + return btcFormatter.formatCoin(trade.getAmount()); + } + + public String getPaymentMethod() { + return trade.getOffer().getPaymentMethodNameWithCountryCode(); + } + + public String getMarketDescription() { + return getCurrencyPair(trade.getOffer().getCurrencyCode()); + } + + @Override + public boolean match(String filterString) { + if (filterString.isEmpty()) { + return true; + } + if (StringUtils.containsIgnoreCase(getTrade().getId(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getAmountAsString(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getPaymentMethod(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getMarketDescription(), filterString)) { + return true; + } + return StringUtils.containsIgnoreCase(getPriceAsString(), filterString); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml index d879d28644..d813b1fbd2 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml @@ -21,12 +21,13 @@ + - + diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 4ff1fa326c..af95e605ba 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -23,11 +23,14 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.PeerInfoIcon; +import bisq.desktop.components.PeerInfoIconTrading; +import bisq.desktop.components.list.FilterBox; import bisq.desktop.main.MainView; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; import bisq.desktop.main.portfolio.PortfolioView; import bisq.desktop.main.portfolio.duplicateoffer.DuplicateOfferView; +import bisq.desktop.main.portfolio.presentation.PortfolioUtil; import bisq.desktop.main.shared.ChatView; import bisq.desktop.util.CssTheme; import bisq.desktop.util.DisplayUtils; @@ -44,6 +47,7 @@ import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.CoinFormatter; import bisq.network.p2p.NodeAddress; @@ -102,6 +106,7 @@ import javafx.event.EventHandler; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.util.Callback; @@ -125,10 +130,13 @@ public class PendingTradesView extends ActivatableViewAndModel tableView; @FXML TableColumn priceColumn, volumeColumn, amountColumn, avatarColumn, marketColumn, roleColumn, paymentMethodColumn, tradeIdColumn, dateColumn, chatColumn, moveTradeToFailedColumn; + private FilteredList filteredList; private SortedList sortedList; private TradeSubView selectedSubView; private EventHandler keyEventEventHandler; @@ -210,16 +218,16 @@ public class PendingTradesView extends ActivatableViewAndModel o.getTrade().getId())); dateColumn.setComparator(Comparator.comparing(o -> o.getTrade().getDate())); - volumeColumn.setComparator(Comparator.comparing(o -> o.getTrade().getTradeVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); - amountColumn.setComparator(Comparator.comparing(o -> o.getTrade().getTradeAmount(), Comparator.nullsFirst(Comparator.naturalOrder()))); - priceColumn.setComparator(Comparator.comparing(item -> FormattingUtils.formatPrice(item.getPrice()))); + volumeColumn.setComparator(Comparator.comparing(o -> o.getTrade().getVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); + amountColumn.setComparator(Comparator.comparing(o -> o.getTrade().getAmount(), Comparator.nullsFirst(Comparator.naturalOrder()))); + priceColumn.setComparator(Comparator.comparing(PendingTradesListItem::getPrice)); paymentMethodColumn.setComparator(Comparator.comparing( item -> item.getTrade().getOffer() != null ? Res.get(item.getTrade().getOffer().getPaymentMethod().getId()) : null, Comparator.nullsFirst(Comparator.naturalOrder()))); - marketColumn.setComparator(Comparator.comparing(model::getMarketLabel)); + marketColumn.setComparator(Comparator.comparing(PendingTradesListItem::getMarketDescription)); roleColumn.setComparator(Comparator.comparing(model::getMyRole)); avatarColumn.setComparator(Comparator.comparing( o -> model.getNumPastTrades(o.getTrade()), @@ -232,12 +240,12 @@ public class PendingTradesView extends ActivatableViewAndModel { final TableRow row = new TableRow<>(); final ContextMenu rowMenu = new ContextMenu(); - MenuItem editItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); - editItem.setOnAction((event) -> { + MenuItem duplicateItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); + duplicateItem.setOnAction((event) -> { try { OfferPayload offerPayload = row.getItem().getTrade().getOffer().getOfferPayload(); if (offerPayload.getPubKeyRing().equals(keyRing.getPubKeyRing())) { - navigation.navigateToWithData(offerPayload, MainView.class, PortfolioView.class, DuplicateOfferView.class); + PortfolioUtil.duplicateOffer(navigation, offerPayload); } else { new Popup().warning(Res.get("portfolio.context.notYourOffer")).show(); } @@ -245,7 +253,7 @@ public class PendingTradesView extends ActivatableViewAndModel list = model.dataModel.list; - sortedList = new SortedList<>(list); + filteredList = new FilteredList<>(list); + sortedList = new SortedList<>(filteredList); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(sortedList); + filterBox.initialize(filteredList, tableView); // here because filteredList is instantiated here + filterBox.activate(); + updateMoveTradeToFailedColumnState(); scene = root.getScene(); @@ -302,12 +314,12 @@ public class PendingTradesView extends ActivatableViewAndModel { + UserThread.execute(() -> onTradeStateChanged(state)); + }); messageStateSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentStartedMessageStateProperty(), this::onMessageStateChanged); } } @@ -299,21 +302,21 @@ public class PendingTradesViewModel extends ActivatableWithDataModel firstHalfOverWarnTextSupplier = () -> ""; private Supplier periodOverWarnTextSupplier = () -> ""; - TradeStepInfo(TitledGroupBg titledGroupBg, Label label, AutoTooltipButton button) { + TradeStepInfo(TitledGroupBg titledGroupBg, + SimpleMarkdownLabel label, + AutoTooltipButton button, + SimpleMarkdownLabel footerLabel) { this.titledGroupBg = titledGroupBg; this.label = label; this.button = button; + this.footerLabel = footerLabel; GridPane.setColumnIndex(button, 0); setState(State.SHOW_GET_HELP_BUTTON); @@ -103,7 +108,7 @@ public class TradeStepInfo { case SHOW_GET_HELP_BUTTON: // grey button titledGroupBg.setText(Res.get("portfolio.pending.support.headline.getHelp")); - label.setText(Res.get("portfolio.pending.support.text.getHelp")); + label.updateContent(""); button.setText(Res.get("portfolio.pending.support.button.getHelp").toUpperCase()); button.setId(null); button.getStyleClass().remove("action-button"); @@ -112,7 +117,7 @@ public class TradeStepInfo { case IN_MEDIATION_SELF_REQUESTED: // red button titledGroupBg.setText(Res.get("portfolio.pending.mediationRequested")); - label.setText(Res.get("portfolio.pending.disputeOpenedMyUser", Res.get("portfolio.pending.communicateWithMediator"))); + label.updateContent(Res.get("portfolio.pending.disputeOpenedMyUser", Res.get("portfolio.pending.communicateWithMediator"))); button.setText(Res.get("portfolio.pending.mediationRequested").toUpperCase()); button.setId("open-dispute-button"); button.getStyleClass().remove("action-button"); @@ -121,7 +126,7 @@ public class TradeStepInfo { case IN_MEDIATION_PEER_REQUESTED: // red button titledGroupBg.setText(Res.get("portfolio.pending.mediationRequested")); - label.setText(Res.get("portfolio.pending.disputeOpenedByPeer", Res.get("portfolio.pending.communicateWithMediator"))); + label.updateContent(Res.get("portfolio.pending.disputeOpenedByPeer", Res.get("portfolio.pending.communicateWithMediator"))); button.setText(Res.get("portfolio.pending.mediationRequested").toUpperCase()); button.setId("open-dispute-button"); button.getStyleClass().remove("action-button"); @@ -130,7 +135,7 @@ public class TradeStepInfo { case MEDIATION_RESULT: // green button titledGroupBg.setText(Res.get("portfolio.pending.mediationResult.headline")); - label.setText(Res.get("portfolio.pending.mediationResult.info.noneAccepted")); + label.updateContent(Res.get("portfolio.pending.mediationResult.info.noneAccepted")); button.setText(Res.get("portfolio.pending.mediationResult.button").toUpperCase()); button.setId(null); button.getStyleClass().add("action-button"); @@ -139,7 +144,7 @@ public class TradeStepInfo { case MEDIATION_RESULT_SELF_ACCEPTED: // green button deactivated titledGroupBg.setText(Res.get("portfolio.pending.mediationResult.headline")); - label.setText(Res.get("portfolio.pending.mediationResult.info.selfAccepted")); + label.updateContent(Res.get("portfolio.pending.mediationResult.info.selfAccepted")); button.setText(Res.get("portfolio.pending.mediationResult.button").toUpperCase()); button.setId(null); button.getStyleClass().add("action-button"); @@ -148,7 +153,7 @@ public class TradeStepInfo { case MEDIATION_RESULT_PEER_ACCEPTED: // green button titledGroupBg.setText(Res.get("portfolio.pending.mediationResult.headline")); - label.setText(Res.get("portfolio.pending.mediationResult.info.peerAccepted")); + label.updateContent(Res.get("portfolio.pending.mediationResult.info.peerAccepted")); button.setText(Res.get("portfolio.pending.mediationResult.button").toUpperCase()); button.setId(null); button.getStyleClass().add("action-button"); @@ -157,7 +162,7 @@ public class TradeStepInfo { case IN_REFUND_REQUEST_SELF_REQUESTED: // red button titledGroupBg.setText(Res.get("portfolio.pending.refundRequested")); - label.setText(Res.get("portfolio.pending.disputeOpenedMyUser", Res.get("portfolio.pending.communicateWithArbitrator"))); + label.updateContent(Res.get("portfolio.pending.disputeOpenedMyUser", Res.get("portfolio.pending.communicateWithArbitrator"))); button.setText(Res.get("portfolio.pending.refundRequested").toUpperCase()); button.setId("open-dispute-button"); button.getStyleClass().remove("action-button"); @@ -166,7 +171,7 @@ public class TradeStepInfo { case IN_REFUND_REQUEST_PEER_REQUESTED: // red button titledGroupBg.setText(Res.get("portfolio.pending.refundRequested")); - label.setText(Res.get("portfolio.pending.disputeOpenedByPeer", Res.get("portfolio.pending.communicateWithArbitrator"))); + label.updateContent(Res.get("portfolio.pending.disputeOpenedByPeer", Res.get("portfolio.pending.communicateWithArbitrator"))); button.setText(Res.get("portfolio.pending.refundRequested").toUpperCase()); button.setId("open-dispute-button"); button.getStyleClass().remove("action-button"); @@ -175,7 +180,7 @@ public class TradeStepInfo { case WARN_HALF_PERIOD: // orange button titledGroupBg.setText(Res.get("portfolio.pending.support.headline.halfPeriodOver")); - label.setText(firstHalfOverWarnTextSupplier.get()); + label.updateContent(firstHalfOverWarnTextSupplier.get()); button.setText(Res.get("portfolio.pending.support.button.getHelp").toUpperCase()); button.setId(null); button.getStyleClass().remove("action-button"); @@ -184,7 +189,7 @@ public class TradeStepInfo { case WARN_PERIOD_OVER: // red button titledGroupBg.setText(Res.get("portfolio.pending.support.headline.periodOver")); - label.setText(periodOverWarnTextSupplier.get()); + label.updateContent(periodOverWarnTextSupplier.get()); button.setText(Res.get("portfolio.pending.openSupport").toUpperCase()); button.setId("open-dispute-button"); button.getStyleClass().remove("action-button"); @@ -195,6 +200,7 @@ public class TradeStepInfo { titledGroupBg.setVisible(false); label.setVisible(false); button.setVisible(false); + footerLabel.setVisible(false); } if (trade != null && trade.getPayoutTx() != null) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java index 006c6e80c7..f05d1a95d0 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java @@ -18,6 +18,7 @@ package bisq.desktop.main.portfolio.pendingtrades; import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.SimpleMarkdownLabel; import bisq.desktop.components.TitledGroupBg; import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import bisq.desktop.main.portfolio.pendingtrades.steps.TradeWizardItem; @@ -26,7 +27,6 @@ import bisq.desktop.util.Layout; import bisq.core.locale.Res; import bisq.core.trade.Trade; -import javafx.scene.control.Label; import javafx.scene.control.Separator; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; @@ -43,7 +43,7 @@ import org.fxmisc.easybind.Subscription; import lombok.extern.slf4j.Slf4j; import static bisq.desktop.util.FormBuilder.addButtonAfterGroup; -import static bisq.desktop.util.FormBuilder.addMultilineLabel; +import static bisq.desktop.util.FormBuilder.addSimpleMarkdownLabel; import static bisq.desktop.util.FormBuilder.addTitledGroupBg; @Slf4j @@ -102,11 +102,14 @@ public abstract class TradeSubView extends HBox { addWizards(); - TitledGroupBg titledGroupBg = addTitledGroupBg(leftGridPane, leftGridPaneRowIndex, 1, "", 30); + TitledGroupBg titledGroupBg = addTitledGroupBg(leftGridPane, ++leftGridPaneRowIndex, 1, "", 10); titledGroupBg.getStyleClass().add("last"); - Label label = addMultilineLabel(leftGridPane, leftGridPaneRowIndex, "", 30); + + SimpleMarkdownLabel label = addSimpleMarkdownLabel(leftGridPane, ++leftGridPaneRowIndex); AutoTooltipButton button = (AutoTooltipButton) addButtonAfterGroup(leftGridPane, ++leftGridPaneRowIndex, ""); - tradeStepInfo = new TradeStepInfo(titledGroupBg, label, button); + SimpleMarkdownLabel footerLabel = addSimpleMarkdownLabel(leftGridPane, ++leftGridPaneRowIndex, Res.get("portfolio.pending.stillNotResolved"), 10); + footerLabel.getStyleClass().add("medium-text"); + tradeStepInfo = new TradeStepInfo(titledGroupBg, label, button, footerLabel); } void showItem(TradeWizardItem item) { @@ -132,7 +135,7 @@ public abstract class TradeSubView extends HBox { void addLineSeparatorToGridPane() { final Separator separator = new Separator(Orientation.VERTICAL); - separator.setMinHeight(22); + separator.setMinHeight(10); GridPane.setMargin(separator, new Insets(0, 0, 0, 13)); GridPane.setHalignment(separator, HPos.LEFT); GridPane.setRowIndex(separator, leftGridPaneRowIndex++); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java index e8b83e0aea..7dce5bfce7 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java @@ -175,6 +175,10 @@ public abstract class TradeStepView extends AnchorPane { } public void activate() { + UserThread.execute(() -> { activateAux(); }); + } + + private void activateAux() { if (makerTxIdTextField != null) { if (makerTxIdSubscription != null) makerTxIdSubscription.unsubscribe(); @@ -226,7 +230,7 @@ public abstract class TradeStepView extends AnchorPane { tradePeriodStateSubscription = EasyBind.subscribe(trade.tradePeriodStateProperty(), newValue -> { if (newValue != null) { - updateTradePeriodState(newValue); + UserThread.execute(() -> updateTradePeriodState(newValue)); } }); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeWizardItem.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeWizardItem.java index afe9f15f39..d0e6d1a714 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeWizardItem.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeWizardItem.java @@ -29,6 +29,8 @@ import org.jetbrains.annotations.NotNull; import static bisq.desktop.util.FormBuilder.getBigIcon; +import bisq.common.UserThread; + public class TradeWizardItem extends Label { private final String iconLabel; @@ -52,20 +54,20 @@ public class TradeWizardItem extends Label { public void setDisabled() { setId("trade-wizard-item-background-disabled"); - setGraphic(getStackPane("trade-step-disabled-bg")); + UserThread.execute(() -> setGraphic(getStackPane("trade-step-disabled-bg"))); } public void setActive() { setId("trade-wizard-item-background-active"); - setGraphic(getStackPane("trade-step-active-bg")); + UserThread.execute(() -> setGraphic(getStackPane("trade-step-active-bg"))); } public void setCompleted() { setId("trade-wizard-item-background-active"); final Text icon = getBigIcon(MaterialDesignIcon.CHECK_CIRCLE); icon.getStyleClass().add("trade-step-active-bg"); - setGraphic(icon); + UserThread.execute(() -> setGraphic(icon)); } @NotNull diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java index a45b38a78e..428ff685a3 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java @@ -21,34 +21,55 @@ import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.BusyAnimation; import bisq.desktop.components.TextFieldWithCopyIcon; import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.components.paymentmethods.AchTransferForm; import bisq.desktop.components.paymentmethods.AdvancedCashForm; import bisq.desktop.components.paymentmethods.AliPayForm; import bisq.desktop.components.paymentmethods.AmazonGiftCardForm; import bisq.desktop.components.paymentmethods.AssetsForm; +import bisq.desktop.components.paymentmethods.BizumForm; +import bisq.desktop.components.paymentmethods.CapitualForm; import bisq.desktop.components.paymentmethods.CashByMailForm; import bisq.desktop.components.paymentmethods.CashDepositForm; +import bisq.desktop.components.paymentmethods.CelPayForm; import bisq.desktop.components.paymentmethods.ChaseQuickPayForm; import bisq.desktop.components.paymentmethods.ClearXchangeForm; +import bisq.desktop.components.paymentmethods.DomesticWireTransferForm; import bisq.desktop.components.paymentmethods.F2FForm; import bisq.desktop.components.paymentmethods.FasterPaymentsForm; import bisq.desktop.components.paymentmethods.HalCashForm; +import bisq.desktop.components.paymentmethods.ImpsForm; import bisq.desktop.components.paymentmethods.InteracETransferForm; import bisq.desktop.components.paymentmethods.JapanBankTransferForm; +import bisq.desktop.components.paymentmethods.MoneseForm; import bisq.desktop.components.paymentmethods.MoneyBeamForm; import bisq.desktop.components.paymentmethods.MoneyGramForm; import bisq.desktop.components.paymentmethods.NationalBankForm; +import bisq.desktop.components.paymentmethods.NeftForm; +import bisq.desktop.components.paymentmethods.NequiForm; +import bisq.desktop.components.paymentmethods.PaxumForm; +import bisq.desktop.components.paymentmethods.PayseraForm; +import bisq.desktop.components.paymentmethods.PaytmForm; import bisq.desktop.components.paymentmethods.PerfectMoneyForm; +import bisq.desktop.components.paymentmethods.PixForm; import bisq.desktop.components.paymentmethods.PopmoneyForm; import bisq.desktop.components.paymentmethods.PromptPayForm; import bisq.desktop.components.paymentmethods.RevolutForm; +import bisq.desktop.components.paymentmethods.RtgsForm; import bisq.desktop.components.paymentmethods.SameBankForm; +import bisq.desktop.components.paymentmethods.SatispayForm; import bisq.desktop.components.paymentmethods.SepaForm; import bisq.desktop.components.paymentmethods.SepaInstantForm; import bisq.desktop.components.paymentmethods.SpecificBankForm; +import bisq.desktop.components.paymentmethods.StrikeForm; +import bisq.desktop.components.paymentmethods.SwiftForm; import bisq.desktop.components.paymentmethods.SwishForm; +import bisq.desktop.components.paymentmethods.TikkieForm; import bisq.desktop.components.paymentmethods.TransferwiseForm; +import bisq.desktop.components.paymentmethods.TransferwiseUsdForm; import bisq.desktop.components.paymentmethods.USPostalMoneyOrderForm; import bisq.desktop.components.paymentmethods.UpholdForm; +import bisq.desktop.components.paymentmethods.UpiForm; +import bisq.desktop.components.paymentmethods.VerseForm; import bisq.desktop.components.paymentmethods.WeChatPayForm; import bisq.desktop.components.paymentmethods.WesternUnionForm; import bisq.desktop.main.MainView; @@ -75,11 +96,12 @@ import bisq.core.payment.payload.HalCashAccountPayload; import bisq.core.payment.payload.MoneyGramAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import bisq.core.payment.payload.SwiftAccountPayload; import bisq.core.payment.payload.USPostalMoneyOrderAccountPayload; import bisq.core.payment.payload.WesternUnionAccountPayload; import bisq.core.trade.Trade; import bisq.core.user.DontShowAgainLookup; - +import bisq.core.util.VolumeUtil; import bisq.common.Timer; import bisq.common.UserThread; import bisq.common.app.DevEnv; @@ -317,9 +339,72 @@ public class BuyerStep2View extends TradeStepView { case PaymentMethod.TRANSFERWISE_ID: gridRow = TransferwiseForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); break; + case PaymentMethod.TRANSFERWISE_USD_ID: + gridRow = TransferwiseUsdForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.PAYSERA_ID: + gridRow = PayseraForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.PAXUM_ID: + gridRow = PaxumForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.NEFT_ID: + gridRow = NeftForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.RTGS_ID: + gridRow = RtgsForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.IMPS_ID: + gridRow = ImpsForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.UPI_ID: + gridRow = UpiForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.PAYTM_ID: + gridRow = PaytmForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.NEQUI_ID: + gridRow = NequiForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.BIZUM_ID: + gridRow = BizumForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.PIX_ID: + gridRow = PixForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; case PaymentMethod.AMAZON_GIFT_CARD_ID: gridRow = AmazonGiftCardForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); break; + case PaymentMethod.CAPITUAL_ID: + gridRow = CapitualForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.CELPAY_ID: + gridRow = CelPayForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.MONESE_ID: + gridRow = MoneseForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.SATISPAY_ID: + gridRow = SatispayForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.TIKKIE_ID: + gridRow = TikkieForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.VERSE_ID: + gridRow = VerseForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.STRIKE_ID: + gridRow = StrikeForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.SWIFT_ID: + gridRow = SwiftForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload, trade); + break; + case PaymentMethod.ACH_TRANSFER_ID: + gridRow = AchTransferForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; + case PaymentMethod.DOMESTIC_WIRE_TRANSFER_ID: + gridRow = DomesticWireTransferForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; default: log.error("Not supported PaymentMethod: " + paymentMethodId); } @@ -542,7 +627,7 @@ public class BuyerStep2View extends TradeStepView { String refTextWarn = Res.get("portfolio.pending.step2_buyer.refTextWarn"); String fees = Res.get("portfolio.pending.step2_buyer.fees"); String id = trade.getShortId(); - String amount = DisplayUtils.formatVolumeWithCode(trade.getTradeVolume()); + String amount = VolumeUtil.formatVolumeWithCode(trade.getVolume()); if (paymentAccountPayload instanceof AssetsAccountPayload) { message += Res.get("portfolio.pending.step2_buyer.altcoin", getCurrencyName(trade), @@ -578,6 +663,10 @@ public class BuyerStep2View extends TradeStepView { } else if (paymentAccountPayload instanceof CashByMailAccountPayload || paymentAccountPayload instanceof HalCashAccountPayload) { message += Res.get("portfolio.pending.step2_buyer.pay", amount); + } else if (paymentAccountPayload instanceof SwiftAccountPayload) { + message += Res.get("portfolio.pending.step2_buyer.pay", amount) + + refTextWarn + "\n\n" + + Res.get("portfolio.pending.step2_buyer.fees.swift"); } else { message += Res.get("portfolio.pending.step2_buyer.pay", amount) + refTextWarn + "\n\n" + diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java index 24db112e4f..c47e684544 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java @@ -18,8 +18,6 @@ package bisq.desktop.main.portfolio.pendingtrades.steps.buyer; import bisq.desktop.components.AutoTooltipButton; -import bisq.desktop.components.AutoTooltipLabel; -import bisq.desktop.components.InputTextField; import bisq.desktop.components.TitledGroupBg; import bisq.desktop.main.MainView; import bisq.desktop.main.overlays.notifications.Notification; @@ -38,10 +36,6 @@ import bisq.core.user.DontShowAgainLookup; import bisq.common.UserThread; import bisq.common.app.DevEnv; -import bisq.common.handlers.FaultHandler; -import bisq.common.handlers.ResultHandler; - -import org.bitcoinj.core.Coin; import com.jfoenix.controls.JFXBadge; @@ -54,20 +48,13 @@ import javafx.scene.layout.Priority; import javafx.geometry.Insets; import javafx.geometry.Pos; -import org.bouncycastle.crypto.params.KeyParameter; - import java.util.concurrent.TimeUnit; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; -import static bisq.desktop.util.FormBuilder.addInputTextField; -import static bisq.desktop.util.FormBuilder.addTitledGroupBg; public class BuyerStep4View extends TradeStepView { - // private final ChangeListener focusedPropertyListener; - private InputTextField withdrawAddressTextField, withdrawMemoTextField; - private Button withdrawToExternalWalletButton, useSavingsWalletButton; - private TitledGroupBg withdrawTitledGroupBg; + private Button closeButton; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation @@ -99,8 +86,13 @@ public class BuyerStep4View extends TradeStepView { gridPane.getColumnConstraints().get(1).setHgrow(Priority.SOMETIMES); TitledGroupBg completedTradeLabel = new TitledGroupBg(); - completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle")); - + if (trade.getDisputeState().isMediated()) { + completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle.mediated")); + } else if (trade.getDisputeState().isArbitrated()) { + completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle.arbitrated")); + } else { + completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle")); + } JFXBadge autoConfBadge = new JFXBadge(new Label(""), Pos.BASELINE_RIGHT); autoConfBadge.setText(Res.get("portfolio.pending.autoConf")); autoConfBadge.getStyleClass().add("auto-conf"); @@ -111,49 +103,28 @@ public class BuyerStep4View extends TradeStepView { GridPane.setRowSpan(hBox2, 5); autoConfBadge.setVisible(AssetTxProofResult.COMPLETED == trade.getAssetTxProofResult()); - addCompactTopLabelTextField(gridPane, gridRow, getBtcTradeAmountLabel(), model.getTradeVolume(), Layout.TWICE_FIRST_ROW_DISTANCE); - addCompactTopLabelTextField(gridPane, ++gridRow, getFiatTradeAmountLabel(), model.getFiatVolume()); - addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("portfolio.pending.step5_buyer.refunded"), model.getSecurityDeposit()); - addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("portfolio.pending.step5_buyer.tradeFee"), model.getTradeFee()); - final String miningFee = model.dataModel.isMaker() ? - Res.get("portfolio.pending.step5_buyer.makersMiningFee") : - Res.get("portfolio.pending.step5_buyer.takersMiningFee"); - addCompactTopLabelTextField(gridPane, ++gridRow, miningFee, model.getTxFee()); - withdrawTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 1, Res.get("portfolio.pending.step5_buyer.withdrawBTC"), Layout.COMPACT_GROUP_DISTANCE); - withdrawTitledGroupBg.getStyleClass().add("last"); - addCompactTopLabelTextField(gridPane, gridRow, Res.get("portfolio.pending.step5_buyer.amount"), model.getPayoutAmount(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + if (trade.getDisputeState().isNotDisputed()) { + addCompactTopLabelTextField(gridPane, gridRow, getBtcTradeAmountLabel(), model.getTradeVolume(), Layout.TWICE_FIRST_ROW_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, getFiatTradeAmountLabel(), model.getFiatVolume()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("portfolio.pending.step5_buyer.refunded"), model.getSecurityDeposit()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("portfolio.pending.step5_buyer.tradeFee"), model.getTradeFee()); + final String miningFee = model.dataModel.isMaker() ? + Res.get("portfolio.pending.step5_buyer.makersMiningFee") : + Res.get("portfolio.pending.step5_buyer.takersMiningFee"); + addCompactTopLabelTextField(gridPane, ++gridRow, miningFee, model.getTxFee()); + } - withdrawAddressTextField = addInputTextField(gridPane, ++gridRow, Res.get("portfolio.pending.step5_buyer.withdrawToAddress")); - withdrawAddressTextField.setManaged(false); - withdrawAddressTextField.setVisible(false); + closeButton = new AutoTooltipButton(Res.get("shared.close")); + closeButton.setDefaultButton(true); + closeButton.getStyleClass().add("action-button"); + GridPane.setRowIndex(closeButton, ++gridRow); + GridPane.setMargin(closeButton, new Insets(Layout.GROUP_DISTANCE, 10, 0, 0)); + gridPane.getChildren().add(closeButton); - withdrawMemoTextField = addInputTextField(gridPane, ++gridRow, - Res.get("funds.withdrawal.memoLabel", Res.getBaseCurrencyCode())); - withdrawMemoTextField.setPromptText(Res.get("funds.withdrawal.memo")); - withdrawMemoTextField.setManaged(false); - withdrawMemoTextField.setVisible(false); - - HBox hBox = new HBox(); - hBox.setSpacing(10); - useSavingsWalletButton = new AutoTooltipButton(Res.get("portfolio.pending.step5_buyer.moveToHavenoWallet")); - useSavingsWalletButton.setDefaultButton(true); - useSavingsWalletButton.getStyleClass().add("action-button"); - Label label = new AutoTooltipLabel(Res.get("shared.OR")); - label.setPadding(new Insets(5, 0, 0, 0)); - withdrawToExternalWalletButton = new AutoTooltipButton(Res.get("portfolio.pending.step5_buyer.withdrawExternal")); - withdrawToExternalWalletButton.setDefaultButton(false); - hBox.getChildren().addAll(useSavingsWalletButton, label, withdrawToExternalWalletButton); - GridPane.setRowIndex(hBox, ++gridRow); - GridPane.setMargin(hBox, new Insets(5, 10, 0, 0)); - gridPane.getChildren().add(hBox); - - useSavingsWalletButton.setOnAction(e -> { + closeButton.setOnAction(e -> { handleTradeCompleted(); model.dataModel.tradeManager.onTradeCompleted(trade); }); - withdrawToExternalWalletButton.setOnAction(e -> { - onWithdrawal(); - }); String key = "tradeCompleted" + trade.getId(); if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { @@ -165,130 +136,8 @@ public class BuyerStep4View extends TradeStepView { } } - private void onWithdrawal() { - withdrawAddressTextField.setManaged(true); - withdrawAddressTextField.setVisible(true); - withdrawMemoTextField.setManaged(true); - withdrawMemoTextField.setVisible(true); - GridPane.setRowSpan(withdrawTitledGroupBg, 3); - withdrawToExternalWalletButton.setDefaultButton(true); - useSavingsWalletButton.setDefaultButton(false); - withdrawToExternalWalletButton.getStyleClass().add("action-button"); - useSavingsWalletButton.getStyleClass().remove("action-button"); - - withdrawToExternalWalletButton.setOnAction(e -> { - if (model.dataModel.isReadyForTxBroadcast()) { - reviewWithdrawal(); - } - }); - - } - - private void reviewWithdrawal() { - throw new RuntimeException("BuyerStep4View.reviewWithdrawal() not yet updated for XMR"); -// Coin amount = trade.getPayoutAmount(); -// BtcWalletService walletService = model.dataModel.btcWalletService; -// -// AddressEntry fromAddressesEntry = walletService.getOrCreateAddressEntry(trade.getId(), AddressEntry.Context.TRADE_PAYOUT); -// String fromAddresses = fromAddressesEntry.getAddressString(); -// String toAddresses = withdrawAddressTextField.getText(); -// if (new BtcAddressValidator().validate(toAddresses).isValid) { -// Coin balance = walletService.getBalanceForAddress(fromAddressesEntry.getAddress()); -// try { -// Transaction feeEstimationTransaction = walletService.getFeeEstimationTransaction(fromAddresses, toAddresses, amount, AddressEntry.Context.TRADE_PAYOUT); -// Coin fee = feeEstimationTransaction.getFee(); -// Coin receiverAmount = amount.subtract(fee); -// if (balance.isZero()) { -// new Popup().warning(Res.get("portfolio.pending.step5_buyer.alreadyWithdrawn")).show(); -// model.dataModel.tradeManager.onTradeCompleted(trade); -// } else { -// if (toAddresses.isEmpty()) { -// validateWithdrawAddress(); -// } else if (Restrictions.isAboveDust(receiverAmount)) { -// CoinFormatter formatter = model.btcFormatter; -// int txVsize = feeEstimationTransaction.getVsize(); -// double feePerVbyte = CoinUtil.getFeePerVbyte(fee, txVsize); -// double vkb = txVsize / 1000d; -// String recAmount = formatter.formatCoinWithCode(receiverAmount); -// new Popup().headLine(Res.get("portfolio.pending.step5_buyer.confirmWithdrawal")) -// .confirmation(Res.get("shared.sendFundsDetailsWithFee", -// formatter.formatCoinWithCode(amount), -// fromAddresses, -// toAddresses, -// formatter.formatCoinWithCode(fee), -// feePerVbyte, -// vkb, -// recAmount)) -// .actionButtonText(Res.get("shared.yes")) -// .onAction(() -> doWithdrawal(amount, fee)) -// .closeButtonText(Res.get("shared.cancel")) -// .onClose(() -> { -// useSavingsWalletButton.setDisable(false); -// withdrawToExternalWalletButton.setDisable(false); -// }) -// .show(); -// } else { -// new Popup().warning(Res.get("portfolio.pending.step5_buyer.amountTooLow")).show(); -// } -// } -// } catch (AddressFormatException e) { -// validateWithdrawAddress(); -// } catch (AddressEntryException e) { -// log.error(e.getMessage()); -// } catch (InsufficientFundsException e) { -// log.error(e.getMessage()); -// e.printStackTrace(); -// new Popup().warning(e.getMessage()).show(); -// } -// } else { -// new Popup().warning(Res.get("validation.btc.invalidAddress")).show(); -// } - } - - private void doWithdrawal(Coin amount, Coin fee) { - String toAddress = withdrawAddressTextField.getText(); - ResultHandler resultHandler = this::handleTradeCompleted; - FaultHandler faultHandler = (errorMessage, throwable) -> { - useSavingsWalletButton.setDisable(false); - withdrawToExternalWalletButton.setDisable(false); - if (throwable != null && throwable.getMessage() != null) - new Popup().error(errorMessage + "\n\n" + throwable.getMessage()).show(); - else - new Popup().error(errorMessage).show(); - }; - if (true) throw new RuntimeException("BuyerStep4View.doWithdrawal() not yet updated for XMR"); -// if (model.dataModel.btcWalletService.isEncrypted()) { -// UserThread.runAfter(() -> model.dataModel.walletPasswordWindow.onAesKey(aesKey -> -// doWithdrawRequest(toAddress, amount, fee, aesKey, resultHandler, faultHandler)) -// .show(), 300, TimeUnit.MILLISECONDS); -// } else -// doWithdrawRequest(toAddress, amount, fee, null, resultHandler, faultHandler); - } - - private void doWithdrawRequest(String toAddress, - Coin amount, - Coin fee, - KeyParameter aesKey, - ResultHandler resultHandler, - FaultHandler faultHandler) { - useSavingsWalletButton.setDisable(true); - withdrawToExternalWalletButton.setDisable(true); - String memo = withdrawMemoTextField.getText(); - if (memo.isEmpty()) { - memo = null; - } - model.dataModel.onWithdrawRequest(toAddress, - amount, - fee, - aesKey, - memo, - resultHandler, - faultHandler); - } - private void handleTradeCompleted() { - useSavingsWalletButton.setDisable(true); - withdrawToExternalWalletButton.setDisable(true); + closeButton.setDisable(true); model.dataModel.xmrWalletService.swapTradeEntryToAvailableEntry(trade.getId(), XmrAddressEntry.Context.TRADE_PAYOUT); openTradeFeedbackWindow(); @@ -297,12 +146,10 @@ public class BuyerStep4View extends TradeStepView { private void openTradeFeedbackWindow() { String key = "feedbackPopupAfterTrade"; if (!DevEnv.isDevMode() && preferences.showAgain(key)) { - UserThread.runAfter(() -> { - new TradeFeedbackWindow() - .dontShowAgainId(key) - .onAction(this::showNavigateToClosedTradesViewPopup) - .show(); - }, 500, TimeUnit.MILLISECONDS); + UserThread.runAfter(() -> new TradeFeedbackWindow() + .dontShowAgainId(key) + .onAction(this::showNavigateToClosedTradesViewPopup) + .show(), 500, TimeUnit.MILLISECONDS); } else { showNavigateToClosedTradesViewPopup(); } @@ -310,23 +157,15 @@ public class BuyerStep4View extends TradeStepView { private void showNavigateToClosedTradesViewPopup() { if (!DevEnv.isDevMode()) { - UserThread.runAfter(() -> { - new Popup().headLine(Res.get("portfolio.pending.step5_buyer.withdrawalCompleted.headline")) - .feedback(Res.get("portfolio.pending.step5_buyer.withdrawalCompleted.msg")) - .actionButtonTextWithGoTo("navigation.portfolio.closedTrades") - .onAction(() -> model.dataModel.navigation.navigateTo(MainView.class, PortfolioView.class, ClosedTradesView.class)) - .dontShowAgainId("tradeCompleteWithdrawCompletedInfo") - .show(); - }, 500, TimeUnit.MILLISECONDS); + UserThread.runAfter(() -> new Popup().headLine(Res.get("portfolio.pending.step5_buyer.tradeCompleted.headline")) + .feedback(Res.get("portfolio.pending.step5_buyer.tradeCompleted.msg")) + .actionButtonTextWithGoTo("navigation.portfolio.closedTrades") + .onAction(() -> model.dataModel.navigation.navigateTo(MainView.class, PortfolioView.class, ClosedTradesView.class)) + .dontShowAgainId("tradeCompleteWithdrawCompletedInfo") + .show(), 500, TimeUnit.MILLISECONDS); } } - private void validateWithdrawAddress() { - withdrawAddressTextField.setValidator(model.btcAddressValidator); - withdrawAddressTextField.requestFocus(); - useSavingsWalletButton.requestFocus(); - } - protected String getBtcTradeAmountLabel() { return Res.get("portfolio.pending.step5_buyer.bought"); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java index 80623f2329..cec5d69c28 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java @@ -22,9 +22,6 @@ import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import bisq.core.locale.Res; -import lombok.extern.slf4j.Slf4j; - -@Slf4j public class SellerStep1View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java index 952b526db7..b079b96d96 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java @@ -48,7 +48,7 @@ import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.trade.txproof.AssetTxProofResult; import bisq.core.user.DontShowAgainLookup; - +import bisq.core.util.VolumeUtil; import bisq.common.Timer; import bisq.common.UserThread; import bisq.common.app.DevEnv; @@ -75,6 +75,9 @@ import java.util.Optional; import javax.annotation.Nullable; import static bisq.desktop.util.FormBuilder.*; +import static bisq.desktop.util.Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE; +import static bisq.desktop.util.Layout.COMPACT_GROUP_DISTANCE; +import static bisq.desktop.util.Layout.FLOATING_LABEL_DISTANCE; import static com.google.common.base.Preconditions.checkNotNull; public class SellerStep3View extends TradeStepView { @@ -119,6 +122,7 @@ public class SellerStep3View extends TradeStepView { case SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT: case SELLER_PUBLISHED_PAYOUT_TX: case SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG: + case SELLER_SENT_PAYMENT_RECEIVED_MSG: busyAnimation.play(); statusLabel.setText(Res.get("shared.sendingConfirmation")); @@ -127,6 +131,7 @@ public class SellerStep3View extends TradeStepView { statusLabel.setText(Res.get("shared.sendingConfirmationAgain")); }, 10); break; + case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG: case SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG: busyAnimation.stop(); statusLabel.setText(Res.get("shared.messageArrived")); @@ -195,7 +200,7 @@ public class SellerStep3View extends TradeStepView { addTradeInfoBlock(); addTitledGroupBg(gridPane, ++gridRow, 3, - Res.get("portfolio.pending.step3_seller.confirmPaymentReceipt"), Layout.COMPACT_GROUP_DISTANCE); + Res.get("portfolio.pending.step3_seller.confirmPaymentReceipt"), COMPACT_GROUP_DISTANCE); TextFieldWithCopyIcon field = addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, Res.get("portfolio.pending.step3_seller.amountToReceive"), @@ -221,15 +226,18 @@ public class SellerStep3View extends TradeStepView { // Not expected myPaymentDetails = ((AssetsAccountPayload) myPaymentAccountPayload).getAddress(); } - peersPaymentDetails = ((AssetsAccountPayload) peersPaymentAccountPayload).getAddress(); + peersPaymentDetails = peersPaymentAccountPayload != null ? + ((AssetsAccountPayload) peersPaymentAccountPayload).getAddress() : "NA"; myTitle = Res.get("portfolio.pending.step3_seller.yourAddress", currencyName); peersTitle = Res.get("portfolio.pending.step3_seller.buyersAddress", currencyName); } else { if (myPaymentDetails.isEmpty()) { // Not expected - myPaymentDetails = myPaymentAccountPayload.getPaymentDetails(); + myPaymentDetails = myPaymentAccountPayload != null ? + myPaymentAccountPayload.getPaymentDetails() : "NA"; } - peersPaymentDetails = peersPaymentAccountPayload.getPaymentDetails(); + peersPaymentDetails = peersPaymentAccountPayload != null ? + peersPaymentAccountPayload.getPaymentDetails() : "NA"; myTitle = Res.get("portfolio.pending.step3_seller.yourAccount"); peersTitle = Res.get("portfolio.pending.step3_seller.buyersAccount"); } @@ -247,7 +255,7 @@ public class SellerStep3View extends TradeStepView { assetTxConfidenceIndicator.setTooltip(new Tooltip()); assetTxProofResultField.setContentForInfoPopOver(createPopoverLabel(Res.get("setting.info.msg"))); - HBox.setMargin(assetTxConfidenceIndicator, new Insets(Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); + HBox.setMargin(assetTxConfidenceIndicator, new Insets(FLOATING_LABEL_DISTANCE, 0, 0, 0)); HBox hBox = new HBox(); HBox.setHgrow(vBox, Priority.ALWAYS); @@ -256,7 +264,10 @@ public class SellerStep3View extends TradeStepView { GridPane.setRowIndex(hBox, gridRow); GridPane.setColumnIndex(hBox, 1); - GridPane.setMargin(hBox, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); + GridPane.setMargin(hBox, new Insets(COMPACT_FIRST_ROW_AND_GROUP_DISTANCE + FLOATING_LABEL_DISTANCE, + 0, + 0, + 0)); gridPane.getChildren().add(hBox); } @@ -288,6 +299,7 @@ public class SellerStep3View extends TradeStepView { Tuple4 tuple = addButtonBusyAnimationLabelAfterGroup(gridPane, ++gridRow, Res.get("portfolio.pending.step3_seller.confirmReceipt")); + HBox hBox = tuple.fourth; GridPane.setColumnSpan(tuple.fourth, 2); confirmButton = tuple.first; confirmButton.setOnAction(e -> onPaymentReceived()); @@ -386,7 +398,7 @@ public class SellerStep3View extends TradeStepView { PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); String key = "confirmPayment" + trade.getId(); String message = ""; - String tradeVolumeWithCode = DisplayUtils.formatVolumeWithCode(trade.getTradeVolume()); + String tradeVolumeWithCode = VolumeUtil.formatVolumeWithCode(trade.getVolume()); String currencyName = getCurrencyName(trade); String part1 = Res.get("portfolio.pending.step3_seller.part", currencyName); if (paymentAccountPayload instanceof AssetsAccountPayload) { @@ -394,7 +406,12 @@ public class SellerStep3View extends TradeStepView { String explorerOrWalletString = isXmrTrade() ? Res.get("portfolio.pending.step3_seller.altcoin.wallet", currencyName) : Res.get("portfolio.pending.step3_seller.altcoin.explorer", currencyName); - message = Res.get("portfolio.pending.step3_seller.altcoin", part1, explorerOrWalletString, address, tradeVolumeWithCode, currencyName); + message = Res.get("portfolio.pending.step3_seller.altcoin", + part1, + explorerOrWalletString, + address, + tradeVolumeWithCode, + currencyName); } else { if (paymentAccountPayload instanceof USPostalMoneyOrderAccountPayload) { message = Res.get("portfolio.pending.step3_seller.postal", part1, tradeVolumeWithCode); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/presentation/PortfolioUtil.java b/desktop/src/main/java/bisq/desktop/main/portfolio/presentation/PortfolioUtil.java new file mode 100644 index 0000000000..ea838ec987 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/presentation/PortfolioUtil.java @@ -0,0 +1,32 @@ +/* + * 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 . + */ + +package bisq.desktop.main.portfolio.presentation; + +import bisq.desktop.Navigation; +import bisq.desktop.main.MainView; +import bisq.desktop.main.portfolio.PortfolioView; +import bisq.desktop.main.portfolio.duplicateoffer.DuplicateOfferView; + +import bisq.core.offer.OfferPayload; + +public class PortfolioUtil { + + public static void duplicateOffer(Navigation navigation, OfferPayload offerPayload) { + navigation.navigateToWithData(offerPayload, MainView.class, PortfolioView.class, DuplicateOfferView.class); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java index d2820e3bc2..230ab67c2d 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java @@ -1316,7 +1316,7 @@ public abstract class DisputeView extends ActivatableView { return; } - String keyBaseUserName = DisputeAgentLookupMap.getKeyBaseUserName(agentNodeAddress.getFullAddress()); + String keyBaseUserName = DisputeAgentLookupMap.getMatrixUserName(agentNodeAddress.getFullAddress()); setText(keyBaseUserName); } else { setText(""); diff --git a/desktop/src/main/java/bisq/desktop/theme-dark.css b/desktop/src/main/java/bisq/desktop/theme-dark.css index 642d99775b..fc3b0ec0aa 100644 --- a/desktop/src/main/java/bisq/desktop/theme-dark.css +++ b/desktop/src/main/java/bisq/desktop/theme-dark.css @@ -27,6 +27,7 @@ /* fifty shades of gray */ -bs-color-gray-13: #bbb; + -bs-color-gray-12: #eaeaea; -bs-color-gray-11: #dadada; -bs-color-gray-10: #cfcecf; -bs-color-gray-6: #afaeb0; @@ -75,7 +76,6 @@ -bs-rd-nav-deselected: rgba(255, 255, 255, 0.45); -bs-rd-nav-button-hover: rgba(255, 255, 255, 0.03); - -bs-tab-content-area: #111; -bs-content-pane-bg-top: #212121; -bs-rd-tab-border: rgba(255, 255, 255, 0.00); -bs-tab-content-label-hover: rgba(0, 0, 0, 0.03); @@ -133,7 +133,7 @@ -bs-chart-tick: rgba(255, 255, 255, 0.7); -bs-chart-lines: rgba(0, 0, 0, 0.3); -bs-white: white; - -bs-prompt-text: -bs-color-gray-3; + -bs-prompt-text: -bs-color-gray-6; -bs-decimals: #db6300; -bs-soft-red: #aa4c3b; -bs-turquoise-light: #11eeee; @@ -141,13 +141,20 @@ /* dao chart colors */ -bs-chart-dao-line1: -bs-color-blue-5; -bs-chart-dao-line2: -bs-color-green-3; - -bs-chart-dao-line3: -bs-turquoise; - -bs-chart-dao-line4: -bs-turquoise-light; + -bs-chart-dao-line3: #0195fe; + -bs-chart-dao-line4: -bs-soft-red; -bs-chart-dao-line5: -bs-yellow; - -bs-chart-dao-line6: -bs-soft-red; + -bs-chart-dao-line6: -bs-turquoise-light; + -bs-chart-dao-line7: #ff6c00; + -bs-chart-dao-line8: -bs-turquoise; + -bs-chart-dao-line9: #7fad01; + -bs-chart-dao-line10: #420080; + -bs-chart-dao-line11: #ff3939; /* Monero orange color code */ -xmr-orange: #f26822; + + -bs-support-chat-background: #cccccc; } /* table view */ @@ -212,9 +219,9 @@ /* text field */ .jfx-text-field, .jfx-text-area, .jfx-combo-box, .jfx-combo-box > .list-cell { - -fx-background-color: derive(-bs-background-color, 8%); - -fx-prompt-text-fill: -bs-color-gray-3; - -fx-text-fill: -bs-color-gray-11; + -fx-background-color: derive(-bs-background-color, 15%); + -fx-prompt-text-fill: -bs-color-gray-6; + -fx-text-fill: -bs-color-gray-12; } .jfx-text-area:readonly, .jfx-text-field:readonly, @@ -380,7 +387,7 @@ .jfx-combo-box:focused > .arrow-button, .jfx-combo-box:focused > .text-input, .jfx-text-field:focused{ - -fx-background-color: derive(-bs-background-color, 8%); + -fx-background-color: derive(-bs-background-color, 15%); } .jfx-combo-box:error, .jfx-text-field:error{ @@ -525,14 +532,24 @@ -fx-background-color: -bs-color-primary-dark; } -.jfx-date-picker .date-picker-popup{ +.jfx-date-picker .date-picker-popup { -fx-background-color: -bs-color-gray-background; } -.jfx-date-picker .left-button, .jfx-date-picker .right-button{ +.jfx-date-picker .left-button, .jfx-date-picker .right-button { -fx-background-color: derive(-bs-color-gray-0, -10%); } .progress-bar > .secondary-bar { -fx-background-color: -bs-color-gray-0; } + +.offer-disabled .label { + -bs-text-color: -bs-color-gray-bbb; +} + +.markdown-label, +.markdown-label * { + -fx-text-fill: -bs-text-color; + -fx-fill: -bs-text-color; +} diff --git a/desktop/src/main/java/bisq/desktop/theme-light.css b/desktop/src/main/java/bisq/desktop/theme-light.css index 6493b64b43..f0abd8fb0b 100644 --- a/desktop/src/main/java/bisq/desktop/theme-light.css +++ b/desktop/src/main/java/bisq/desktop/theme-light.css @@ -1,6 +1,6 @@ .root { - -bs-color-primary: #f1482d; - -bs-color-primary-dark: #c43f3d; + -bs-color-primary: #25b135; + -bs-color-primary-dark: #2ea33c; -bs-text-color: #000000; -bs-background-color: #ffffff; -bs-background-gray: #dddddd; @@ -108,13 +108,20 @@ /* dao chart colors */ -bs-chart-dao-line1: -bs-color-blue-5; -bs-chart-dao-line2: -bs-color-green-3; - -bs-chart-dao-line3: -bs-turquoise; - -bs-chart-dao-line4: -bs-turquoise-light; + -bs-chart-dao-line3: #0195fe; + -bs-chart-dao-line4: -bs-soft-red; -bs-chart-dao-line5: -bs-yellow; - -bs-chart-dao-line6: -bs-soft-red; + -bs-chart-dao-line6: -bs-turquoise-light; + -bs-chart-dao-line7: #ff6c00; + -bs-chart-dao-line8: -bs-turquoise; + -bs-chart-dao-line9: #7fad01; + -bs-chart-dao-line10: #420080; + -bs-chart-dao-line11: #ff3939; /* Monero orange color code */ -xmr-orange: #f26822; + + -bs-support-chat-background: #4b4b4b; } .warning-box { diff --git a/desktop/src/main/java/bisq/desktop/util/Colors.java b/desktop/src/main/java/bisq/desktop/util/Colors.java index 80b1fdc911..87821ce160 100644 --- a/desktop/src/main/java/bisq/desktop/util/Colors.java +++ b/desktop/src/main/java/bisq/desktop/util/Colors.java @@ -24,4 +24,10 @@ public class Colors { public static final Paint BLUE = Color.valueOf("#0f87c3"); public static final Paint LIGHT_GREY = Color.valueOf("#CCCCCC"); public static final Paint GREEN = Color.valueOf("#00aa33"); + + public static final Color AVATAR_RED = Color.rgb(255, 0, 0); + public static final Color AVATAR_ORANGE = Color.rgb(255, 140, 0); + public static final Color AVATAR_BLUE = Color.rgb(0, 139, 205); + public static final Color AVATAR_GREEN = Color.rgb(0, 225, 0); + public static final Color AVATAR_GREY = Color.rgb(128, 128, 128); } diff --git a/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java b/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java index 92ef911b91..afe74aa655 100644 --- a/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java +++ b/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java @@ -1,35 +1,36 @@ package bisq.desktop.util; +import bisq.core.account.witness.AccountAgeWitness; +import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.GlobalSettings; import bisq.core.locale.Res; -import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; import bisq.core.util.FormattingUtils; +import bisq.core.offer.OfferDirection; +import bisq.core.payment.PaymentAccount; import bisq.core.util.ParsingUtils; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.CoinFormatter; +import bisq.common.crypto.PubKeyRing; + import org.bitcoinj.core.Coin; -import org.bitcoinj.core.Monetary; -import org.bitcoinj.utils.Fiat; -import org.bitcoinj.utils.MonetaryFormat; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DurationFormatUtils; import java.text.DateFormat; -import java.text.DecimalFormat; -import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Date; -import java.util.Locale; import java.util.Optional; import lombok.extern.slf4j.Slf4j; @@ -37,7 +38,6 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class DisplayUtils { private static final int SCALE = 3; - private static final MonetaryFormat FIAT_VOLUME_FORMAT = new MonetaryFormat().shift(0).minDecimals(0).repeatOptionalDecimals(0, 0); public static String formatDateTime(Date date) { return FormattingUtils.formatDateTime(date, true); @@ -80,6 +80,30 @@ public class DisplayUtils { } } + public static String getAccountWitnessDescription(AccountAgeWitnessService accountAgeWitnessService, + PaymentMethod paymentMethod, + PaymentAccountPayload paymentAccountPayload, + PubKeyRing pubKeyRing) { + String description = Res.get("peerInfoIcon.tooltip.unknownAge"); + Optional aaw = accountAgeWitnessService.findWitness(paymentAccountPayload, pubKeyRing); + if (aaw.isPresent()) { + long accountAge = accountAgeWitnessService.getAccountAge(aaw.get(), new Date()); + long signAge = -1L; + if (PaymentMethod.hasChargebackRisk(paymentMethod)) { + signAge = accountAgeWitnessService.getWitnessSignAge(aaw.get(), new Date()); + } + if (signAge > -1) { + description = Res.get("peerInfo.age.chargeBackRisk") + ": " + formatAccountAge(signAge); + } else if (accountAge > -1) { + description = Res.get("peerInfoIcon.tooltip.age", formatAccountAge(accountAge)); + if (PaymentMethod.hasChargebackRisk(paymentMethod)) { + description += ", " + Res.get("offerbook.timeSinceSigning.notSigned"); + } + } + } + return description; + } + public static String formatAccountAge(long durationMillis) { durationMillis = Math.max(0, durationMillis); String day = Res.get("time.day").toLowerCase(); @@ -92,100 +116,22 @@ public class DisplayUtils { return value ? Res.get("shared.yes") : Res.get("shared.no"); } - /////////////////////////////////////////////////////////////////////////////////////////// - // Volume - /////////////////////////////////////////////////////////////////////////////////////////// - - public static String formatVolume(Offer offer, Boolean decimalAligned, int maxNumberOfDigits) { - return formatVolume(offer, decimalAligned, maxNumberOfDigits, true); - } - - public static String formatVolume(Offer offer, Boolean decimalAligned, int maxNumberOfDigits, boolean showRange) { - String formattedVolume = offer.isRange() && showRange ? formatVolume(offer.getMinVolume()) + FormattingUtils.RANGE_SEPARATOR + formatVolume(offer.getVolume()) : formatVolume(offer.getVolume()); - - if (decimalAligned) { - formattedVolume = FormattingUtils.fillUpPlacesWithEmptyStrings(formattedVolume, maxNumberOfDigits); - } - return formattedVolume; - } - - public static String formatLargeFiat(double value, String currency) { - if (value <= 0) { - return "0"; - } - NumberFormat numberFormat = DecimalFormat.getInstance(Locale.US); - numberFormat.setGroupingUsed(true); - return numberFormat.format(value) + " " + currency; - } - - public static String formatLargeFiatWithUnitPostFix(double value, String currency) { - if (value <= 0) { - return "0"; - } - String[] units = new String[]{"", "K", "M", "B"}; - int digitGroups = (int) (Math.log10(value) / Math.log10(1000)); - return new DecimalFormat("#,##0.###").format(value / Math.pow(1000, digitGroups)) + units[digitGroups] + " " + currency; - } - - public static String formatVolume(Volume volume) { - return formatVolume(volume, FIAT_VOLUME_FORMAT, false); - } - - private static String formatVolume(Volume volume, MonetaryFormat fiatVolumeFormat, boolean appendCurrencyCode) { - if (volume != null) { - Monetary monetary = volume.getMonetary(); - if (monetary instanceof Fiat) - return FormattingUtils.formatFiat((Fiat) monetary, fiatVolumeFormat, appendCurrencyCode); - else - return FormattingUtils.formatAltcoinVolume((Altcoin) monetary, appendCurrencyCode); - } else { - return ""; - } - } - - public static String formatVolumeWithCode(Volume volume) { - return formatVolume(volume, true); - } - - public static String formatVolume(Volume volume, boolean appendCode) { - return formatVolume(volume, FIAT_VOLUME_FORMAT, appendCode); - } - - public static String formatAverageVolumeWithCode(Volume volume) { - return formatVolume(volume, FIAT_VOLUME_FORMAT.minDecimals(2), true); - } - - public static String formatVolumeLabel(String currencyCode) { - return formatVolumeLabel(currencyCode, ""); - } - - public static String formatVolumeLabel(String currencyCode, String postFix) { - return Res.get("formatter.formatVolumeLabel", - currencyCode, postFix); - } - /////////////////////////////////////////////////////////////////////////////////////////// // Offer direction /////////////////////////////////////////////////////////////////////////////////////////// - public static String getDirectionWithCode(OfferPayload.Direction direction, String currencyCode) { + public static String getDirectionWithCode(OfferDirection direction, String currencyCode) { if (CurrencyUtil.isFiatCurrency(currencyCode)) - return (direction == OfferPayload.Direction.BUY) ? Res.get("shared.buyCurrency", Res.getBaseCurrencyCode()) : Res.get("shared.sellCurrency", Res.getBaseCurrencyCode()); + return (direction == OfferDirection.BUY) ? Res.get("shared.buyCurrency", Res.getBaseCurrencyCode()) : Res.get("shared.sellCurrency", Res.getBaseCurrencyCode()); else - return (direction == OfferPayload.Direction.SELL) ? Res.get("shared.buyCurrency", currencyCode) : Res.get("shared.sellCurrency", currencyCode); + return (direction == OfferDirection.SELL) ? Res.get("shared.buyCurrency", currencyCode) : Res.get("shared.sellCurrency", currencyCode); } - public static String getDirectionBothSides(OfferPayload.Direction direction, String currencyCode) { - if (CurrencyUtil.isFiatCurrency(currencyCode)) { - currencyCode = Res.getBaseCurrencyCode(); - return direction == OfferPayload.Direction.BUY ? - Res.get("formatter.makerTaker", currencyCode, Res.get("shared.buyer"), currencyCode, Res.get("shared.seller")) : - Res.get("formatter.makerTaker", currencyCode, Res.get("shared.seller"), currencyCode, Res.get("shared.buyer")); - } else { - return direction == OfferPayload.Direction.SELL ? - Res.get("formatter.makerTaker", currencyCode, Res.get("shared.buyer"), currencyCode, Res.get("shared.seller")) : - Res.get("formatter.makerTaker", currencyCode, Res.get("shared.seller"), currencyCode, Res.get("shared.buyer")); - } + public static String getDirectionBothSides(OfferDirection direction) { + String currencyCode = Res.getBaseCurrencyCode(); + return direction == OfferDirection.BUY ? + Res.get("formatter.makerTaker", currencyCode, Res.get("shared.buyer"), currencyCode, Res.get("shared.seller")) : + Res.get("formatter.makerTaker", currencyCode, Res.get("shared.seller"), currencyCode, Res.get("shared.buyer")); } public static String getDirectionForBuyer(boolean isMyOffer, String currencyCode) { @@ -214,28 +160,28 @@ public class DisplayUtils { } } - public static String getDirectionForTakeOffer(OfferPayload.Direction direction, String currencyCode) { + public static String getDirectionForTakeOffer(OfferDirection direction, String currencyCode) { String baseCurrencyCode = Res.getBaseCurrencyCode(); if (CurrencyUtil.isFiatCurrency(currencyCode)) { - return direction == OfferPayload.Direction.BUY ? + return direction == OfferDirection.BUY ? Res.get("formatter.youAre", Res.get("shared.selling"), baseCurrencyCode, Res.get("shared.buying"), currencyCode) : Res.get("formatter.youAre", Res.get("shared.buying"), baseCurrencyCode, Res.get("shared.selling"), currencyCode); } else { - return direction == OfferPayload.Direction.SELL ? + return direction == OfferDirection.SELL ? Res.get("formatter.youAre", Res.get("shared.selling"), currencyCode, Res.get("shared.buying"), baseCurrencyCode) : Res.get("formatter.youAre", Res.get("shared.buying"), currencyCode, Res.get("shared.selling"), baseCurrencyCode); } } - public static String getOfferDirectionForCreateOffer(OfferPayload.Direction direction, String currencyCode) { + public static String getOfferDirectionForCreateOffer(OfferDirection direction, String currencyCode) { String baseCurrencyCode = Res.getBaseCurrencyCode(); if (CurrencyUtil.isFiatCurrency(currencyCode)) { - return direction == OfferPayload.Direction.BUY ? + return direction == OfferDirection.BUY ? Res.get("formatter.youAreCreatingAnOffer.fiat", Res.get("shared.buy"), baseCurrencyCode) : Res.get("formatter.youAreCreatingAnOffer.fiat", Res.get("shared.sell"), baseCurrencyCode); } else { - return direction == OfferPayload.Direction.SELL ? + return direction == OfferDirection.SELL ? Res.get("formatter.youAreCreatingAnOffer.altcoin", Res.get("shared.buy"), currencyCode, Res.get("shared.selling"), baseCurrencyCode) : Res.get("formatter.youAreCreatingAnOffer.altcoin", Res.get("shared.sell"), currencyCode, Res.get("shared.buying"), baseCurrencyCode); } @@ -284,7 +230,7 @@ public class DisplayUtils { CoinFormatter formatter) { String feeInBtc = makerFeeAsCoin != null ? formatter.formatCoinWithCode(makerFeeAsCoin) : Res.get("shared.na"); if (optionalFeeInFiat != null && optionalFeeInFiat.isPresent()) { - String feeInFiat = formatAverageVolumeWithCode(optionalFeeInFiat.get()); + String feeInFiat = VolumeUtil.formatAverageVolumeWithCode(optionalFeeInFiat.get()); return Res.get("feeOptionWindow.fee", feeInBtc, feeInFiat); } else { return feeInBtc; @@ -330,4 +276,21 @@ public class DisplayUtils { public static Coin reduceTo4Decimals(Coin coin, CoinFormatter coinFormatter) { return ParsingUtils.parseToCoin(coinFormatter.formatCoin(coin), coinFormatter); } + + public static String createAccountName(String paymentMethodId, String name) { + name = name.trim(); + name = StringUtils.abbreviate(name, 9); + String method = Res.get(paymentMethodId); + return method.concat(": ").concat(name); + } + + public static String createAssetsAccountName(PaymentAccount paymentAccount, String address) { + String currency = paymentAccount.getSingleTradeCurrency() != null ? paymentAccount.getSingleTradeCurrency().getCode() : ""; + return createAssetsAccountName(currency, address); + } + + public static String createAssetsAccountName(String currency, String address) { + address = StringUtils.abbreviate(address, 9); + return currency.concat(": ").concat(address); + } } diff --git a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java index 1e5f8c5791..79cea95dc2 100644 --- a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java @@ -28,6 +28,7 @@ import bisq.desktop.components.BalanceTextField; import bisq.desktop.components.HavenoTextArea; import bisq.desktop.components.HavenoTextField; import bisq.desktop.components.BusyAnimation; +import bisq.desktop.components.ExplorerAddressTextField; import bisq.desktop.components.ExternalHyperlink; import bisq.desktop.components.FundsTextField; import bisq.desktop.components.HyperlinkWithIcon; @@ -35,6 +36,7 @@ import bisq.desktop.components.InfoInputTextField; import bisq.desktop.components.InfoTextField; import bisq.desktop.components.InputTextField; import bisq.desktop.components.PasswordTextField; +import bisq.desktop.components.SimpleMarkdownLabel; import bisq.desktop.components.TextFieldWithCopyIcon; import bisq.desktop.components.TextFieldWithIcon; import bisq.desktop.components.TitledGroupBg; @@ -45,6 +47,7 @@ import bisq.core.locale.Res; import bisq.common.util.Tuple2; import bisq.common.util.Tuple3; import bisq.common.util.Tuple4; +import bisq.common.util.Utilities; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; @@ -184,6 +187,23 @@ public class FormBuilder { } + /////////////////////////////////////////////////////////////////////////////////////////// + // Simple Markdown Label + /////////////////////////////////////////////////////////////////////////////////////////// + public static SimpleMarkdownLabel addSimpleMarkdownLabel(GridPane gridPane, int rowIndex) { + return addSimpleMarkdownLabel(gridPane, rowIndex, null, 0); + } + + public static SimpleMarkdownLabel addSimpleMarkdownLabel(GridPane gridPane, int rowIndex, String markdown, double top) { + SimpleMarkdownLabel label = new SimpleMarkdownLabel(markdown); + + GridPane.setRowIndex(label, rowIndex); + GridPane.setMargin(label, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(label); + + return label; + } + /////////////////////////////////////////////////////////////////////////////////////////// // Multiline Label /////////////////////////////////////////////////////////////////////////////////////////// @@ -346,19 +366,19 @@ public class FormBuilder { textField.setPrefWidth(Layout.INITIAL_WINDOW_WIDTH); Button button = new AutoTooltipButton("..."); - button.setStyle("-fx-min-width: 35px; -fx-pref-height: 20; -fx-padding: 3 3 3 3; -fx-border-insets: 5px;"); + button.setStyle("-fx-min-width: 26; -fx-pref-height: 26; -fx-padding: 0 0 10 0; -fx-background-color: -fx-background;"); button.managedProperty().bind(button.visibleProperty()); - VBox vBoxButton = new VBox(button); - vBoxButton.setAlignment(Pos.CENTER); - HBox hBox2 = new HBox(textField, vBoxButton); - Label label = getTopLabel(title); - VBox textFieldVbox = getTopLabelVBox(0); - textFieldVbox.getChildren().addAll(label, hBox2); + HBox hbox = new HBox(textField, button); + hbox.setAlignment(Pos.CENTER_LEFT); + hbox.setSpacing(8); - gridPane.getChildren().add(textFieldVbox); - GridPane.setRowIndex(textFieldVbox, rowIndex); - GridPane.setMargin(textFieldVbox, new Insets(Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); + VBox vbox = getTopLabelVBox(0); + vbox.getChildren().addAll(getTopLabel(title), hbox); + + gridPane.getChildren().add(vbox); + GridPane.setRowIndex(vbox, rowIndex); + GridPane.setMargin(vbox, new Insets(Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); return new Tuple2<>(textField, button); } @@ -391,10 +411,37 @@ public class FormBuilder { return new Tuple2<>(label1, label2); } + public static Tuple2 addConfirmationLabelTextField(GridPane gridPane, + int rowIndex, + String title1, + String title2) { + return addConfirmationLabelTextField(gridPane, rowIndex, title1, title2, 0); + } + + public static Tuple2 addConfirmationLabelTextField(GridPane gridPane, + int rowIndex, + String title1, + String title2, + double top) { + Label label1 = addLabel(gridPane, rowIndex, title1); + label1.getStyleClass().add("confirmation-label"); + TextField label2 = new HavenoTextField(title2); + gridPane.getChildren().add(label2); + label2.getStyleClass().add("confirmation-text-field-as-label"); + label2.setEditable(false); + label2.setFocusTraversable(false); + GridPane.setRowIndex(label2, rowIndex); + GridPane.setColumnIndex(label2, 1); + GridPane.setMargin(label1, new Insets(top, 0, 0, 0)); + GridPane.setHalignment(label1, HPos.LEFT); + GridPane.setMargin(label2, new Insets(top, 0, 0, 0)); + return new Tuple2<>(label1, label2); + } + public static Tuple2 addConfirmationLabelLabelWithCopyIcon(GridPane gridPane, - int rowIndex, - String title1, - String title2) { + int rowIndex, + String title1, + String title2) { Label label1 = addLabel(gridPane, rowIndex, title1); label1.getStyleClass().add("confirmation-label"); TextFieldWithCopyIcon label2 = new TextFieldWithCopyIcon("confirmation-value"); @@ -444,7 +491,6 @@ public class FormBuilder { double top) { TextFieldWithIcon textFieldWithIcon = new TextFieldWithIcon(); - textFieldWithIcon.setMouseTransparent(true); textFieldWithIcon.setFocusTraversable(false); return new Tuple2<>(addTopLabelWithVBox(gridPane, rowIndex, columnIndex, title, textFieldWithIcon, top).first, textFieldWithIcon); @@ -698,6 +744,24 @@ public class FormBuilder { return new Tuple2<>(label, txTextField); } + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + ExplorerAddressTextField + /////////////////////////////////////////////////////////////////////////////////////////// + public static void addLabelExplorerAddressTextField(GridPane gridPane, + int rowIndex, + String title, + String address) { + Label label = addLabel(gridPane, rowIndex, title, 0); + label.getStyleClass().add("confirmation-label"); + GridPane.setHalignment(label, HPos.LEFT); + + ExplorerAddressTextField addressTextField = new ExplorerAddressTextField(); + addressTextField.setup(address); + GridPane.setRowIndex(addressTextField, rowIndex); + GridPane.setColumnIndex(addressTextField, 1); + gridPane.getChildren().add(addressTextField); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Label + InputTextField @@ -820,6 +884,25 @@ public class FormBuilder { } + public static Tuple3 addTopLabelInputTextFieldSlideToggleButtonRight(GridPane gridPane, + int rowIndex, + String title, + String toggleButtonTitle) { + + InputTextField inputTextField = new InputTextField(); + Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, inputTextField, 0); + ToggleButton toggleButton = new JFXToggleButton(); + toggleButton.setText(toggleButtonTitle); + HBox hBox = new HBox(); + hBox.getChildren().addAll(topLabelWithVBox.second, toggleButton); + HBox.setMargin(toggleButton, new Insets(9, 0, 0, 0)); + gridPane.add(hBox, 0, rowIndex); + GridPane.setMargin(hBox, new Insets(Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); + return new Tuple3<>(topLabelWithVBox.first, inputTextField, toggleButton); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// // Label + InputTextField + Button /////////////////////////////////////////////////////////////////////////////////////////// @@ -1228,6 +1311,29 @@ public class FormBuilder { return new Tuple2<>(label, vBox); } + public static Tuple3 addTopLabelTextFieldWithHbox(GridPane gridPane, + int rowIndex, + String titleTextfield, + double top) { + HBox hBox = new HBox(); + hBox.setSpacing(10); + + TextField textField = new HavenoTextField(); + + final VBox topLabelVBox = getTopLabelVBox(5); + final Label topLabel = getTopLabel(titleTextfield); + topLabelVBox.getChildren().addAll(topLabel, textField); + + hBox.getChildren().addAll(topLabelVBox); + + GridPane.setRowIndex(hBox, rowIndex); + GridPane.setMargin(hBox, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(hBox); + + return new Tuple3<>(topLabel, textField, hBox); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Label + ComboBox /////////////////////////////////////////////////////////////////////////////////////////// @@ -1831,6 +1937,15 @@ public class FormBuilder { return button; } + public static Button addCloseButton(GridPane gridPane, int rowIndex, Runnable closeHandler) { + Button closeButton = addButtonAfterGroup(gridPane, rowIndex, Res.get("shared.close")); + GridPane.setColumnIndex(closeButton, 1); + GridPane.setHalignment(closeButton, HPos.RIGHT); + + closeButton.setOnAction(e -> closeHandler.run()); + + return closeButton; + } /////////////////////////////////////////////////////////////////////////////////////////// // Button + Button @@ -2019,7 +2134,7 @@ public class FormBuilder { public static Tuple3 getEditableValueBoxWithInfo(String promptText) { InfoInputTextField infoInputTextField = new InfoInputTextField(60); InputTextField input = infoInputTextField.getInputTextField(); - input.setPromptText(promptText); + input.setPromptText(Utilities.toTruncatedString(promptText, 28)); Label label = new AutoTooltipLabel(Res.getBaseCurrencyCode()); label.getStyleClass().add("input-label"); @@ -2163,7 +2278,11 @@ public class FormBuilder { } public static Text getRegularIconForLabel(GlyphIcons icon, Label label) { - return getIconForLabel(icon, "1.231em", label); + return getRegularIconForLabel(icon, label, null); + } + + public static Text getRegularIconForLabel(GlyphIcons icon, Label label, String style) { + return getIconForLabel(icon, "1.231em", label, style); } public static Text getIcon(GlyphIcons icon) { @@ -2197,6 +2316,14 @@ public class FormBuilder { return label; } + public static Label getIcon(AwesomeIcon icon, String fontSize) { + return getIconForLabel(icon, new Label(), fontSize); + } + + public static Label getSmallIcon(AwesomeIcon icon) { + return getIcon(icon, "1em"); + } + public static Label getIconForLabel(AwesomeIcon icon, Label label, String fontSize) { AwesomeDude.setIcon(label, icon, fontSize); return label; diff --git a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java index d381ffab14..e8a0838c21 100644 --- a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java @@ -37,7 +37,6 @@ import bisq.core.locale.CountryUtil; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; -import bisq.core.offer.OfferRestrictions; import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccountList; import bisq.core.payment.payload.PaymentMethod; @@ -97,13 +96,16 @@ import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.ScrollBar; +import javafx.scene.control.ScrollPane; import javafx.scene.control.TableView; import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; - +import javafx.scene.layout.Priority; +import javafx.geometry.HPos; import javafx.geometry.Orientation; import javafx.collections.FXCollections; @@ -155,6 +157,8 @@ public class GUIUtil { private static FeeService feeService; private static Preferences preferences; + + public static TradeCurrency TOP_ALTCOIN = CurrencyUtil.getTradeCurrency("ETH").get(); public static void setFeeService(FeeService feeService) { GUIUtil.feeService = feeService; @@ -541,7 +545,7 @@ public class GUIUtil { HBox box = new HBox(); box.setSpacing(20); Label paymentType = new AutoTooltipLabel( - method.isAsset() ? Res.get("shared.crypto") : Res.get("shared.fiat")); + method.isAltcoin() ? Res.get("shared.crypto") : Res.get("shared.fiat")); paymentType.getStyleClass().add("currency-label-small"); Label paymentMethod = new AutoTooltipLabel(Res.get(id)); @@ -695,18 +699,18 @@ public class GUIUtil { return t.cast(parent); } - public static void showTakeOfferFromUnsignedAccountWarning(CoinFormatter coinFormatter) { + public static void showTakeOfferFromUnsignedAccountWarning() { String key = "confirmTakeOfferFromUnsignedAccount"; - new Popup().warning(Res.get("payment.takeOfferFromUnsignedAccount.warning", coinFormatter.formatCoinWithCode(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT))) + new Popup().warning(Res.get("payment.takeOfferFromUnsignedAccount.warning")) .width(900) .closeButtonText(Res.get("shared.iConfirm")) .dontShowAgainId(key) .show(); } - public static void showMakeOfferToUnsignedAccountWarning(CoinFormatter coinFormatter) { + public static void showMakeOfferToUnsignedAccountWarning() { String key = "confirmMakeOfferToUnsignedAccount"; - new Popup().warning(Res.get("payment.makeOfferToUnsignedAccount.warning", coinFormatter.formatCoinWithCode(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT))) + new Popup().warning(Res.get("payment.makeOfferToUnsignedAccount.warning")) .width(900) .closeButtonText(Res.get("shared.iConfirm")) .dontShowAgainId(key) @@ -1089,4 +1093,35 @@ public class GUIUtil { return result.name(); } } + + public static ScrollPane createScrollPane() { + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scrollPane.setFitToWidth(true); + scrollPane.setFitToHeight(true); + AnchorPane.setLeftAnchor(scrollPane, 0d); + AnchorPane.setTopAnchor(scrollPane, 0d); + AnchorPane.setRightAnchor(scrollPane, 0d); + AnchorPane.setBottomAnchor(scrollPane, 0d); + return scrollPane; + } + + public static void setDefaultTwoColumnConstraintsForGridPane(GridPane gridPane) { + ColumnConstraints columnConstraints1 = new ColumnConstraints(); + columnConstraints1.setHalignment(HPos.RIGHT); + columnConstraints1.setHgrow(Priority.NEVER); + columnConstraints1.setMinWidth(200); + ColumnConstraints columnConstraints2 = new ColumnConstraints(); + columnConstraints2.setHgrow(Priority.ALWAYS); + gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); + } + + public static void updateTopAltcoin(Preferences preferences) { + TradeCurrency tradeCurrency = preferences.getPreferredTradeCurrency(); + if (CurrencyUtil.isFiatCurrency(tradeCurrency.getCode())) { + return; + } + TOP_ALTCOIN = tradeCurrency; + } } diff --git a/desktop/src/main/java/bisq/desktop/util/Layout.java b/desktop/src/main/java/bisq/desktop/util/Layout.java index 6cce07cea5..e2478fdcf4 100644 --- a/desktop/src/main/java/bisq/desktop/util/Layout.java +++ b/desktop/src/main/java/bisq/desktop/util/Layout.java @@ -25,7 +25,7 @@ public class Layout { public static final double FIRST_ROW_DISTANCE = 20d; public static final double COMPACT_FIRST_ROW_DISTANCE = 10d; public static final double TWICE_FIRST_ROW_DISTANCE = 20d * 2; - public static final double FLOATING_LABEL_DISTANCE = 20d; + public static final double FLOATING_LABEL_DISTANCE = 18d; public static final double GROUP_DISTANCE = 40d; public static final double COMPACT_GROUP_DISTANCE = 30d; public static final double GROUP_DISTANCE_WITHOUT_SEPARATOR = 20d; diff --git a/desktop/src/main/java/bisq/desktop/util/Transitions.java b/desktop/src/main/java/bisq/desktop/util/Transitions.java index d5596e28ee..f94efdb4c4 100644 --- a/desktop/src/main/java/bisq/desktop/util/Transitions.java +++ b/desktop/src/main/java/bisq/desktop/util/Transitions.java @@ -92,12 +92,12 @@ public class Transitions { public void fadeOutAndRemove(Node node, int duration, EventHandler handler) { FadeTransition fade = fadeOut(node, getDuration(duration)); fade.setInterpolator(Interpolator.EASE_IN); - fade.setOnFinished(actionEvent -> UserThread.execute(() -> { + fade.setOnFinished(actionEvent -> { ((Pane) (node.getParent())).getChildren().remove(node); //Profiler.printMsgWithTime("fadeOutAndRemove"); if (handler != null) handler.handle(actionEvent); - })); + }); } // Blur diff --git a/desktop/src/main/java/bisq/desktop/util/filtering/FilterableListItem.java b/desktop/src/main/java/bisq/desktop/util/filtering/FilterableListItem.java new file mode 100644 index 0000000000..304237d965 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/util/filtering/FilterableListItem.java @@ -0,0 +1,22 @@ +/* + * 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 . + */ + +package bisq.desktop.util.filtering; + +public interface FilterableListItem { + boolean match(String filterString); +} diff --git a/desktop/src/main/java/bisq/desktop/util/filtering/FilteringUtils.java b/desktop/src/main/java/bisq/desktop/util/filtering/FilteringUtils.java new file mode 100644 index 0000000000..eced5cb7e7 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/util/filtering/FilteringUtils.java @@ -0,0 +1,70 @@ +/* + * 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 . + */ + +package bisq.desktop.util.filtering; + +import bisq.core.offer.Offer; +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; + +import org.apache.commons.lang3.StringUtils; + +public class FilteringUtils { + public static boolean match(Offer offer, String filterString) { + if (StringUtils.containsIgnoreCase(offer.getId(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(offer.getPaymentMethod().getDisplayString(), filterString)) { + return true; + } + return offer.getOfferFeePaymentTxId() != null && StringUtils.containsIgnoreCase(offer.getOfferFeePaymentTxId(), filterString); + } + + public static boolean match(Trade trade, String filterString) { + if (trade == null) { + return false; + } + if (trade.getTakerFeeTxId() != null && StringUtils.containsIgnoreCase(trade.getTakerFeeTxId(), filterString)) { + return true; + } + if (trade.getMaker().getDepositTxHash() != null && StringUtils.containsIgnoreCase(trade.getMaker().getDepositTxHash(), filterString)) { + return true; + } + if (trade.getTaker().getDepositTxHash() != null && StringUtils.containsIgnoreCase(trade.getTaker().getDepositTxHash(), filterString)) { + return true; + } + if (trade.getPayoutTxId() != null && StringUtils.containsIgnoreCase(trade.getPayoutTxId(), filterString)) { + return true; + } + + // match contract + boolean isBuyerOnion = false; + boolean isSellerOnion = false; + boolean matchesBuyersPaymentAccountData = false; + boolean matchesSellersPaymentAccountData = false; + if (trade.getContract() != null) { + isBuyerOnion = StringUtils.containsIgnoreCase(trade.getContract().getBuyerNodeAddress().getFullAddress(), filterString); + isSellerOnion = StringUtils.containsIgnoreCase(trade.getContract().getSellerNodeAddress().getFullAddress(), filterString); + matchesBuyersPaymentAccountData = trade.getBuyer().getPaymentAccountPayload() != null && + StringUtils.containsIgnoreCase(trade.getBuyer().getPaymentAccountPayload().getPaymentDetails(), filterString); + matchesSellersPaymentAccountData = trade.getSeller().getPaymentAccountPayload() != null && + StringUtils.containsIgnoreCase(trade.getSeller().getPaymentAccountPayload().getPaymentDetails(), filterString); + } + return isBuyerOnion || isSellerOnion || + matchesBuyersPaymentAccountData || matchesSellersPaymentAccountData; + } +} diff --git a/desktop/src/main/java/bisq/desktop/util/validation/BtcValidator.java b/desktop/src/main/java/bisq/desktop/util/validation/BtcValidator.java index a73c7282f6..5bd7f44b58 100644 --- a/desktop/src/main/java/bisq/desktop/util/validation/BtcValidator.java +++ b/desktop/src/main/java/bisq/desktop/util/validation/BtcValidator.java @@ -22,6 +22,7 @@ import bisq.core.locale.Res; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.NumberValidator; + import org.bitcoinj.core.Coin; import javax.inject.Inject; @@ -44,7 +45,6 @@ public class BtcValidator extends NumberValidator { @Nullable @Setter - @Getter protected Coin maxValue; @Nullable diff --git a/desktop/src/main/java/bisq/desktop/util/validation/CapitualValidator.java b/desktop/src/main/java/bisq/desktop/util/validation/CapitualValidator.java new file mode 100644 index 0000000000..f94b933340 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/util/validation/CapitualValidator.java @@ -0,0 +1,24 @@ +package bisq.desktop.util.validation; + +import bisq.core.locale.Res; +import bisq.core.util.validation.InputValidator; +import bisq.core.util.validation.RegexValidator; + +import javax.inject.Inject; + +public class CapitualValidator extends InputValidator { + private final RegexValidator regexValidator; + + @Inject + public CapitualValidator(RegexValidator regexValidator) { + regexValidator.setPattern("CAP-[A-Za-z0-9]{6}"); + regexValidator.setErrorMessage(Res.get("validation.capitual.invalidFormat")); + this.regexValidator = regexValidator; + } + + @Override + public ValidationResult validate(String input) { + + return regexValidator.validate(input); + } +} diff --git a/desktop/src/main/java/bisq/desktop/util/validation/FiatVolumeValidator.java b/desktop/src/main/java/bisq/desktop/util/validation/FiatVolumeValidator.java index 35be39dda3..1674c96574 100644 --- a/desktop/src/main/java/bisq/desktop/util/validation/FiatVolumeValidator.java +++ b/desktop/src/main/java/bisq/desktop/util/validation/FiatVolumeValidator.java @@ -18,6 +18,7 @@ package bisq.desktop.util.validation; import bisq.core.util.validation.MonetaryValidator; + import javax.inject.Inject; public class FiatVolumeValidator extends MonetaryValidator { diff --git a/desktop/src/main/java/bisq/desktop/util/validation/IBANValidator.java b/desktop/src/main/java/bisq/desktop/util/validation/IBANValidator.java index 54648bf642..cb44e5d827 100644 --- a/desktop/src/main/java/bisq/desktop/util/validation/IBANValidator.java +++ b/desktop/src/main/java/bisq/desktop/util/validation/IBANValidator.java @@ -24,12 +24,22 @@ import java.math.BigInteger; import java.util.Locale; -// TODO Does not yet recognize special letters like ä, ö, ü, å, ... as invalid characters -public final class IBANValidator extends InputValidator { +import lombok.Setter; +// TODO Does not yet recognize special letters like ä, ö, ü, å, ... as invalid characters +public class IBANValidator extends InputValidator { + + @Setter + private String restrictToCountry = ""; /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// + public IBANValidator() { + } + + public IBANValidator(String restrictToCountry) { + this.restrictToCountry = restrictToCountry; + } @Override public ValidationResult validate(String input) { @@ -45,6 +55,8 @@ public final class IBANValidator extends InputValidator { // check if country code is letters and checksum numeric if (!(Character.isLetter(input.charAt(0)) && Character.isLetter(input.charAt(1)))) return new ValidationResult(false, Res.get("validation.iban.invalidCountryCode")); + if (restrictToCountry.length() > 0 && !restrictToCountry.equals(input.substring(0, 2))) + return new ValidationResult(false, Res.get("validation.iban.invalidCountryCode")); if (!(Character.isDigit(input.charAt(2)) && Character.isDigit(input.charAt(3)))) return new ValidationResult(false, Res.get("validation.iban.checkSumNotNumeric")); diff --git a/desktop/src/main/java/bisq/desktop/util/validation/PercentageNumberValidator.java b/desktop/src/main/java/bisq/desktop/util/validation/PercentageNumberValidator.java index 56f789fbb9..83e14db61c 100644 --- a/desktop/src/main/java/bisq/desktop/util/validation/PercentageNumberValidator.java +++ b/desktop/src/main/java/bisq/desktop/util/validation/PercentageNumberValidator.java @@ -19,6 +19,7 @@ package bisq.desktop.util.validation; import bisq.core.locale.Res; import bisq.core.util.validation.NumberValidator; + import lombok.Setter; import javax.annotation.Nullable; diff --git a/desktop/src/main/java/bisq/desktop/util/validation/SecurityDepositValidator.java b/desktop/src/main/java/bisq/desktop/util/validation/SecurityDepositValidator.java index c31ac130ea..2ae725d781 100644 --- a/desktop/src/main/java/bisq/desktop/util/validation/SecurityDepositValidator.java +++ b/desktop/src/main/java/bisq/desktop/util/validation/SecurityDepositValidator.java @@ -23,6 +23,7 @@ import bisq.core.payment.PaymentAccount; import bisq.core.util.FormattingUtils; import bisq.core.util.ParsingUtils; import bisq.core.util.validation.NumberValidator; + import javax.inject.Inject; public class SecurityDepositValidator extends NumberValidator { diff --git a/desktop/src/main/java/bisq/desktop/util/validation/SepaIBANValidator.java b/desktop/src/main/java/bisq/desktop/util/validation/SepaIBANValidator.java new file mode 100644 index 0000000000..3541e397e7 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/util/validation/SepaIBANValidator.java @@ -0,0 +1,31 @@ +package bisq.desktop.util.validation; + +import bisq.core.locale.Country; +import bisq.core.locale.CountryUtil; +import bisq.core.locale.Res; + +import java.util.List; +import java.util.Optional; + +public class SepaIBANValidator extends IBANValidator { + + @Override + public ValidationResult validate(String input) { + ValidationResult result = super.validate(input); + + if (result.isValid) { + List sepaCountries = CountryUtil.getAllSepaCountries(); + String ibanCountryCode = input.substring(0, 2).toUpperCase(); + Optional ibanCountry = sepaCountries + .stream() + .filter(c -> c.code.equals(ibanCountryCode)) + .findFirst(); + + if (!ibanCountry.isPresent()) { + return new ValidationResult(false, Res.get("validation.iban.sepaNotSupported")); + } + } + + return result; + } +} diff --git a/desktop/src/test/java/bisq/desktop/GuiceSetupTest.java b/desktop/src/test/java/bisq/desktop/GuiceSetupTest.java index 82a2aadddc..c08c203289 100644 --- a/desktop/src/test/java/bisq/desktop/GuiceSetupTest.java +++ b/desktop/src/test/java/bisq/desktop/GuiceSetupTest.java @@ -99,7 +99,7 @@ public class GuiceSetupTest { assertSingleton(DisplayedTransactionsFactory.class); // core module -// assertSingleton(HavenoSetup.class); // this is a can of worms +// assertSingleton(BisqSetup.class); // this is a can of worms // assertSingleton(DisputeMsgEvents.class); assertSingleton(TorSetup.class); assertSingleton(P2PNetworkSetup.class); diff --git a/desktop/src/test/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModelTest.java index 7573b10c77..db4ea05c06 100644 --- a/desktop/src/test/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModelTest.java @@ -32,8 +32,8 @@ import javafx.collections.ObservableList; import org.junit.Before; import org.junit.Test; -import static bisq.desktop.main.offer.offerbook.OfferBookListItemMaker.btcBuyItem; -import static bisq.desktop.main.offer.offerbook.OfferBookListItemMaker.btcSellItem; +import static bisq.desktop.main.offer.offerbook.OfferBookListItemMaker.xmrBuyItem; +import static bisq.desktop.main.offer.offerbook.OfferBookListItemMaker.xmrSellItem; import static bisq.desktop.maker.PreferenceMakers.empty; import static bisq.desktop.maker.TradeCurrencyMakers.usd; import static com.natpryce.makeiteasy.MakeItEasy.make; @@ -57,7 +57,7 @@ public class OfferBookChartViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, empty, null, null, null); + final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, null, null, null); assertEquals(0, model.maxPlacesForBuyPrice.intValue()); } @@ -68,7 +68,7 @@ public class OfferBookChartViewModelTest { final ObservableList offerBookListItems = FXCollections.observableArrayList(); - final OfferBookListItem item = make(OfferBookListItemMaker.btcBuyItem.but(with(OfferBookListItemMaker.useMarketBasedPrice, true))); + final OfferBookListItem item = make(OfferBookListItemMaker.xmrBuyItem.but(with(OfferBookListItemMaker.useMarketBasedPrice, true))); item.getOffer().setPriceFeedService(priceFeedService); offerBookListItems.addAll(item); @@ -76,7 +76,7 @@ public class OfferBookChartViewModelTest { when(priceFeedService.updateCounterProperty()).thenReturn(new SimpleIntegerProperty()); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, empty, priceFeedService, null, null); + final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, priceFeedService, null, null); model.activate(); assertEquals(0, model.maxPlacesForBuyPrice.intValue()); } @@ -86,16 +86,16 @@ public class OfferBookChartViewModelTest { OfferBook offerBook = mock(OfferBook.class); PriceFeedService service = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); - offerBookListItems.addAll(make(OfferBookListItemMaker.btcBuyItem)); + offerBookListItems.addAll(make(OfferBookListItemMaker.xmrBuyItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, empty, service, null, null); + final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); assertEquals(7, model.maxPlacesForBuyPrice.intValue()); - offerBookListItems.addAll(make(btcBuyItem.but(with(OfferBookListItemMaker.price, 94016475L)))); + offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.price, 94016475L)))); assertEquals(9, model.maxPlacesForBuyPrice.intValue()); // 9401.6475 - offerBookListItems.addAll(make(btcBuyItem.but(with(OfferBookListItemMaker.price, 101016475L)))); + offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.price, 101016475L)))); assertEquals(10, model.maxPlacesForBuyPrice.intValue()); //10101.6475 } @@ -106,7 +106,7 @@ public class OfferBookChartViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, empty, null, null, null); + final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, null, null, null); assertEquals(0, model.maxPlacesForBuyVolume.intValue()); } @@ -115,16 +115,16 @@ public class OfferBookChartViewModelTest { OfferBook offerBook = mock(OfferBook.class); PriceFeedService service = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); - offerBookListItems.addAll(make(OfferBookListItemMaker.btcBuyItem)); + offerBookListItems.addAll(make(OfferBookListItemMaker.xmrBuyItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, empty, service, null, null); + final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); assertEquals(1, model.maxPlacesForBuyVolume.intValue()); //0 - offerBookListItems.addAll(make(btcBuyItem.but(with(OfferBookListItemMaker.amount, 100000000L)))); + offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.amount, 100000000L)))); assertEquals(2, model.maxPlacesForBuyVolume.intValue()); //10 - offerBookListItems.addAll(make(btcBuyItem.but(with(OfferBookListItemMaker.amount, 22128600000L)))); + offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.amount, 22128600000L)))); assertEquals(4, model.maxPlacesForBuyVolume.intValue()); //2213 } @@ -135,7 +135,7 @@ public class OfferBookChartViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, empty, null, null, null); + final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, null, null, null); assertEquals(0, model.maxPlacesForSellPrice.intValue()); } @@ -146,7 +146,7 @@ public class OfferBookChartViewModelTest { final ObservableList offerBookListItems = FXCollections.observableArrayList(); - final OfferBookListItem item = make(OfferBookListItemMaker.btcSellItem.but(with(OfferBookListItemMaker.useMarketBasedPrice, true))); + final OfferBookListItem item = make(OfferBookListItemMaker.xmrSellItem.but(with(OfferBookListItemMaker.useMarketBasedPrice, true))); item.getOffer().setPriceFeedService(priceFeedService); offerBookListItems.addAll(item); @@ -154,7 +154,7 @@ public class OfferBookChartViewModelTest { when(priceFeedService.updateCounterProperty()).thenReturn(new SimpleIntegerProperty()); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, empty, priceFeedService, null, null); + final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, priceFeedService, null, null); model.activate(); assertEquals(0, model.maxPlacesForSellPrice.intValue()); } @@ -164,16 +164,16 @@ public class OfferBookChartViewModelTest { OfferBook offerBook = mock(OfferBook.class); PriceFeedService service = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); - offerBookListItems.addAll(make(OfferBookListItemMaker.btcSellItem)); + offerBookListItems.addAll(make(OfferBookListItemMaker.xmrSellItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, empty, service, null, null); + final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); assertEquals(7, model.maxPlacesForSellPrice.intValue()); // 10.0000 default price - offerBookListItems.addAll(make(btcSellItem.but(with(OfferBookListItemMaker.price, 94016475L)))); + offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.price, 94016475L)))); assertEquals(9, model.maxPlacesForSellPrice.intValue()); // 9401.6475 - offerBookListItems.addAll(make(btcSellItem.but(with(OfferBookListItemMaker.price, 101016475L)))); + offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.price, 101016475L)))); assertEquals(10, model.maxPlacesForSellPrice.intValue()); // 10101.6475 } @@ -184,7 +184,7 @@ public class OfferBookChartViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, empty, null, null, null); + final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, null, null, null); assertEquals(0, model.maxPlacesForSellVolume.intValue()); } @@ -193,16 +193,16 @@ public class OfferBookChartViewModelTest { OfferBook offerBook = mock(OfferBook.class); PriceFeedService service = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); - offerBookListItems.addAll(make(OfferBookListItemMaker.btcSellItem)); + offerBookListItems.addAll(make(OfferBookListItemMaker.xmrSellItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, empty, service, null, null); + final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); assertEquals(1, model.maxPlacesForSellVolume.intValue()); //0 - offerBookListItems.addAll(make(btcSellItem.but(with(OfferBookListItemMaker.amount, 100000000L)))); + offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.amount, 100000000L)))); assertEquals(2, model.maxPlacesForSellVolume.intValue()); //10 - offerBookListItems.addAll(make(btcSellItem.but(with(OfferBookListItemMaker.amount, 22128600000L)))); + offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.amount, 22128600000L)))); assertEquals(4, model.maxPlacesForSellVolume.intValue()); //2213 } } diff --git a/desktop/src/test/java/bisq/desktop/main/market/spread/SpreadViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/market/spread/SpreadViewModelTest.java index 521dea5445..01d63aadad 100644 --- a/desktop/src/test/java/bisq/desktop/main/market/spread/SpreadViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/market/spread/SpreadViewModelTest.java @@ -33,8 +33,8 @@ import javafx.collections.ObservableList; import org.junit.Test; -import static bisq.desktop.main.offer.offerbook.OfferBookListItemMaker.btcBuyItem; -import static bisq.desktop.main.offer.offerbook.OfferBookListItemMaker.btcSellItem; +import static bisq.desktop.main.offer.offerbook.OfferBookListItemMaker.xmrBuyItem; +import static bisq.desktop.main.offer.offerbook.OfferBookListItemMaker.xmrSellItem; import static bisq.desktop.main.offer.offerbook.OfferBookListItemMaker.id; import static com.natpryce.makeiteasy.MakeItEasy.make; import static com.natpryce.makeiteasy.MakeItEasy.with; @@ -61,14 +61,14 @@ public class SpreadViewModelTest { public void testMaxCharactersForAmount() { OfferBook offerBook = mock(OfferBook.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); - offerBookListItems.addAll(make(btcBuyItem)); + offerBookListItems.addAll(make(xmrBuyItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); SpreadViewModel model = new SpreadViewModel(offerBook, null, coinFormatter); model.activate(); assertEquals(6, model.maxPlacesForAmount.intValue()); // 0.001 - offerBookListItems.addAll(make(btcBuyItem.but(with(OfferBookListItemMaker.amount, 1403000000L)))); + offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.amount, 1403000000L)))); assertEquals(7, model.maxPlacesForAmount.intValue()); //14.0300 } @@ -77,7 +77,7 @@ public class SpreadViewModelTest { OfferBook offerBook = mock(OfferBook.class); PriceFeedService priceFeedService = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); - offerBookListItems.addAll(make(btcBuyItem)); + offerBookListItems.addAll(make(xmrBuyItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); @@ -86,10 +86,10 @@ public class SpreadViewModelTest { assertEquals(1, model.spreadItems.get(0).numberOfOffers); - offerBookListItems.addAll(make(btcBuyItem.but(with(id, "2345"))), - make(btcBuyItem.but(with(id, "2345"))), - make(btcSellItem.but(with(id, "3456"))), - make(btcSellItem.but(with(id, "3456")))); + offerBookListItems.addAll(make(xmrBuyItem.but(with(id, "2345"))), + make(xmrBuyItem.but(with(id, "2345"))), + make(xmrSellItem.but(with(id, "3456"))), + make(xmrSellItem.but(with(id, "3456")))); assertEquals(2, model.spreadItems.get(0).numberOfBuyOffers); assertEquals(1, model.spreadItems.get(0).numberOfSellOffers); diff --git a/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java index 49ff38a0eb..4311d4260d 100644 --- a/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java @@ -35,6 +35,8 @@ import org.bitcoinj.utils.Fiat; import javafx.collections.FXCollections; import javafx.collections.ObservableSet; +import javafx.util.Pair; + import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -45,6 +47,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; +import java.util.Map; import java.util.Set; import org.junit.Before; @@ -94,11 +97,10 @@ public class TradesChartsViewModelTest { false, null, null, - 1, + 0, null, null, - null - ); + null); @Before public void setup() throws IOException { @@ -115,7 +117,8 @@ public class TradesChartsViewModelTest { @SuppressWarnings("ConstantConditions") @Test public void testGetCandleData() { - model.selectedTradeCurrencyProperty.setValue(new FiatCurrency("EUR")); + String currencyCode = "EUR"; + model.selectedTradeCurrencyProperty.setValue(new FiatCurrency(currencyCode)); long low = Fiat.parseFiat("EUR", "500").value; long open = Fiat.parseFiat("EUR", "520").value; @@ -163,7 +166,13 @@ public class TradesChartsViewModelTest { null, null)); - CandleData candleData = model.getCandleData(model.roundToTick(now, TradesChartsViewModel.TickUnit.DAY).getTime(), set, 0); + Map>> itemsPerInterval = null; + long tick = ChartCalculations.roundToTick(now, TradesChartsViewModel.TickUnit.DAY).getTime(); + CandleData candleData = ChartCalculations.getCandleData(tick, + set, + 0, + TradesChartsViewModel.TickUnit.DAY, currencyCode, + itemsPerInterval); assertEquals(open, candleData.open); assertEquals(close, candleData.close); assertEquals(high, candleData.high); diff --git a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModelTest.java index d5b4fb1ccb..d62be51ba2 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModelTest.java @@ -7,6 +7,7 @@ import bisq.core.locale.FiatCurrency; import bisq.core.locale.GlobalSettings; import bisq.core.locale.Res; import bisq.core.offer.CreateOfferService; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferUtil; import bisq.core.payment.ClearXchangeAccount; import bisq.core.payment.PaymentAccount; @@ -27,7 +28,6 @@ import java.util.UUID; import org.junit.Before; import org.junit.Test; -import static bisq.core.offer.OfferPayload.Direction; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -96,7 +96,7 @@ public class CreateOfferDataModelTest { when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount); when(offerUtil.getMakerFee(any())).thenReturn(Coin.ZERO); - model.initWithData(Direction.BUY, new FiatCurrency("USD")); + model.initWithData(OfferDirection.BUY, new FiatCurrency("USD")); assertEquals("USD", model.getTradeCurrencyCode().get()); } @@ -118,7 +118,7 @@ public class CreateOfferDataModelTest { when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount); when(offerUtil.getMakerFee(any())).thenReturn(Coin.ZERO); - model.initWithData(Direction.BUY, new FiatCurrency("USD")); + model.initWithData(OfferDirection.BUY, new FiatCurrency("USD")); assertEquals("USD", model.getTradeCurrencyCode().get()); } } diff --git a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java index 776131fbf0..6ca4dbdd90 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java @@ -28,6 +28,7 @@ import bisq.core.locale.CryptoCurrency; import bisq.core.locale.GlobalSettings; import bisq.core.locale.Res; import bisq.core.offer.CreateOfferService; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferUtil; import bisq.core.payment.PaymentAccount; @@ -128,7 +129,7 @@ public class CreateOfferViewModelTest { coinFormatter, tradeStats, null); - dataModel.initWithData(OfferPayload.Direction.BUY, new CryptoCurrency("XMR", "monero")); + dataModel.initWithData(OfferDirection.BUY, new CryptoCurrency("XMR", "monero")); dataModel.activate(); model = new CreateOfferViewModel(dataModel, diff --git a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookListItemMaker.java b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookListItemMaker.java index 1c59cb3d87..82fe7a7ed0 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookListItemMaker.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookListItemMaker.java @@ -19,14 +19,14 @@ package bisq.desktop.main.offer.offerbook; import bisq.desktop.maker.OfferMaker; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferDirection; import com.natpryce.makeiteasy.Instantiator; import com.natpryce.makeiteasy.MakeItEasy; import com.natpryce.makeiteasy.Maker; import com.natpryce.makeiteasy.Property; -import static bisq.desktop.maker.OfferMaker.btcUsdOffer; +import static bisq.desktop.maker.OfferMaker.xmrUsdOffer; import static com.natpryce.makeiteasy.MakeItEasy.a; import static com.natpryce.makeiteasy.MakeItEasy.make; import static com.natpryce.makeiteasy.MakeItEasy.with; @@ -37,18 +37,18 @@ public class OfferBookListItemMaker { public static final Property price = new Property<>(); public static final Property amount = new Property<>(); public static final Property minAmount = new Property<>(); - public static final Property direction = new Property<>(); + public static final Property direction = new Property<>(); public static final Property useMarketBasedPrice = new Property<>(); public static final Property marketPriceMargin = new Property<>(); public static final Property baseCurrencyCode = new Property<>(); public static final Property counterCurrencyCode = new Property<>(); public static final Instantiator OfferBookListItem = lookup -> - new OfferBookListItem(make(btcUsdOffer.but( + new OfferBookListItem(make(xmrUsdOffer.but( MakeItEasy.with(OfferMaker.price, lookup.valueOf(price, 100000L)), with(OfferMaker.amount, lookup.valueOf(amount, 100000L)), with(OfferMaker.minAmount, lookup.valueOf(amount, 100000L)), - with(OfferMaker.direction, lookup.valueOf(direction, OfferPayload.Direction.BUY)), + with(OfferMaker.direction, lookup.valueOf(direction, OfferDirection.BUY)), with(OfferMaker.useMarketBasedPrice, lookup.valueOf(useMarketBasedPrice, false)), with(OfferMaker.marketPriceMargin, lookup.valueOf(marketPriceMargin, 0.0)), with(OfferMaker.baseCurrencyCode, lookup.valueOf(baseCurrencyCode, "XMR")), @@ -57,13 +57,13 @@ public class OfferBookListItemMaker { ))); public static final Instantiator OfferBookListItemWithRange = lookup -> - new OfferBookListItem(make(btcUsdOffer.but( + new OfferBookListItem(make(xmrUsdOffer.but( MakeItEasy.with(OfferMaker.price, lookup.valueOf(price, 100000L)), with(OfferMaker.minAmount, lookup.valueOf(minAmount, 100000L)), with(OfferMaker.amount, lookup.valueOf(amount, 200000L))))); - public static final Maker btcBuyItem = a(OfferBookListItem); - public static final Maker btcSellItem = a(OfferBookListItem, with(direction, OfferPayload.Direction.SELL)); + public static final Maker xmrBuyItem = a(OfferBookListItem); + public static final Maker xmrSellItem = a(OfferBookListItem, with(direction, OfferDirection.SELL)); - public static final Maker btcItemWithRange = a(OfferBookListItemWithRange); + public static final Maker xmrItemWithRange = a(OfferBookListItemWithRange); } diff --git a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java index 0c6e9f1117..f25c29ff08 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java @@ -23,8 +23,8 @@ import bisq.core.locale.FiatCurrency; import bisq.core.locale.GlobalSettings; import bisq.core.locale.Res; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; import bisq.core.offer.OpenOfferManager; +import bisq.core.offer.OfferPayload; import bisq.core.payment.AliPayAccount; import bisq.core.payment.CountryBasedPaymentAccount; import bisq.core.payment.CryptoCurrencyAccount; @@ -42,6 +42,7 @@ import bisq.core.payment.payload.SpecificBanksAccountPayload; import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.User; import bisq.core.util.PriceUtil; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.ImmutableCoinFormatter; @@ -75,7 +76,9 @@ import static com.natpryce.makeiteasy.MakeItEasy.make; import static com.natpryce.makeiteasy.MakeItEasy.with; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -83,12 +86,15 @@ import static org.mockito.Mockito.when; public class OfferBookViewModelTest { private final CoinFormatter coinFormatter = new ImmutableCoinFormatter(Config.baseCurrencyNetworkParameters().getMonetaryFormat()); private static final Logger log = LoggerFactory.getLogger(OfferBookViewModelTest.class); + private User user; @Before public void setUp() { GlobalSettings.setDefaultTradeCurrency(usd); Res.setBaseCurrencyCode(usd.getCode()); Res.setBaseCurrencyName(usd.getName()); + user = mock(User.class); + when(user.hasPaymentAccountForCurrency(any())).thenReturn(true); } private PriceUtil getPriceUtil() { @@ -236,8 +242,8 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, - null, null, null, getPriceUtil(), null, coinFormatter); + final OfferBookViewModel model = new BtcOfferBookViewModel(null, null, offerBook, empty, null, null, null, + null, null, null, getPriceUtil(), null, coinFormatter, null); assertEquals(0, model.maxPlacesForAmount.intValue()); } @@ -246,16 +252,16 @@ public class OfferBookViewModelTest { OfferBook offerBook = mock(OfferBook.class); OpenOfferManager openOfferManager = mock(OpenOfferManager.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); - offerBookListItems.addAll(make(OfferBookListItemMaker.btcBuyItem)); + offerBookListItems.addAll(make(OfferBookListItemMaker.xmrBuyItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, - null, null, null, getPriceUtil(), null, coinFormatter); + final OfferBookViewModel model = new BtcOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, + null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); assertEquals(6, model.maxPlacesForAmount.intValue()); - offerBookListItems.addAll(make(btcBuyItem.but(with(amount, 2000000000L)))); + offerBookListItems.addAll(make(xmrBuyItem.but(with(amount, 2000000000L)))); assertEquals(7, model.maxPlacesForAmount.intValue()); } @@ -264,18 +270,18 @@ public class OfferBookViewModelTest { OfferBook offerBook = mock(OfferBook.class); OpenOfferManager openOfferManager = mock(OpenOfferManager.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); - offerBookListItems.addAll(make(OfferBookListItemMaker.btcItemWithRange)); + offerBookListItems.addAll(make(OfferBookListItemMaker.xmrItemWithRange)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, - null, null, null, getPriceUtil(), null, coinFormatter); + final OfferBookViewModel model = new BtcOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, + null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); assertEquals(15, model.maxPlacesForAmount.intValue()); - offerBookListItems.addAll(make(btcItemWithRange.but(with(amount, 2000000000L)))); + offerBookListItems.addAll(make(xmrItemWithRange.but(with(amount, 2000000000L)))); assertEquals(16, model.maxPlacesForAmount.intValue()); - offerBookListItems.addAll(make(btcItemWithRange.but(with(minAmount, 30000000000L), + offerBookListItems.addAll(make(xmrItemWithRange.but(with(minAmount, 30000000000L), with(amount, 30000000000L)))); assertEquals(19, model.maxPlacesForAmount.intValue()); } @@ -287,8 +293,8 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, - null, null, null, getPriceUtil(), null, coinFormatter); + final OfferBookViewModel model = new BtcOfferBookViewModel(null, null, offerBook, empty, null, null, null, + null, null, null, getPriceUtil(), null, coinFormatter, null); assertEquals(0, model.maxPlacesForVolume.intValue()); } @@ -297,16 +303,16 @@ public class OfferBookViewModelTest { OfferBook offerBook = mock(OfferBook.class); OpenOfferManager openOfferManager = mock(OpenOfferManager.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); - offerBookListItems.addAll(make(OfferBookListItemMaker.btcBuyItem)); + offerBookListItems.addAll(make(OfferBookListItemMaker.xmrBuyItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, - null, null, null, getPriceUtil(), null, coinFormatter); + final OfferBookViewModel model = new BtcOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, + null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); assertEquals(5, model.maxPlacesForVolume.intValue()); - offerBookListItems.addAll(make(btcBuyItem.but(with(amount, 2000000000L)))); + offerBookListItems.addAll(make(xmrBuyItem.but(with(amount, 2000000000L)))); assertEquals(7, model.maxPlacesForVolume.intValue()); } @@ -315,18 +321,18 @@ public class OfferBookViewModelTest { OfferBook offerBook = mock(OfferBook.class); OpenOfferManager openOfferManager = mock(OpenOfferManager.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); - offerBookListItems.addAll(make(OfferBookListItemMaker.btcItemWithRange)); + offerBookListItems.addAll(make(OfferBookListItemMaker.xmrItemWithRange)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, - null, null, null, getPriceUtil(), null, coinFormatter); + final OfferBookViewModel model = new BtcOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, + null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); assertEquals(9, model.maxPlacesForVolume.intValue()); - offerBookListItems.addAll(make(btcItemWithRange.but(with(amount, 2000000000L)))); + offerBookListItems.addAll(make(xmrItemWithRange.but(with(amount, 2000000000L)))); assertEquals(11, model.maxPlacesForVolume.intValue()); - offerBookListItems.addAll(make(btcItemWithRange.but(with(minAmount, 30000000000L), + offerBookListItems.addAll(make(xmrItemWithRange.but(with(minAmount, 30000000000L), with(amount, 30000000000L)))); assertEquals(19, model.maxPlacesForVolume.intValue()); } @@ -338,8 +344,8 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, - null, null, null, getPriceUtil(), null, coinFormatter); + final OfferBookViewModel model = new BtcOfferBookViewModel(null, null, offerBook, empty, null, null, null, + null, null, null, getPriceUtil(), null, coinFormatter, null); assertEquals(0, model.maxPlacesForPrice.intValue()); } @@ -348,18 +354,18 @@ public class OfferBookViewModelTest { OfferBook offerBook = mock(OfferBook.class); OpenOfferManager openOfferManager = mock(OpenOfferManager.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); - offerBookListItems.addAll(make(OfferBookListItemMaker.btcBuyItem)); + offerBookListItems.addAll(make(OfferBookListItemMaker.xmrBuyItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, - null, null, null, getPriceUtil(), null, coinFormatter); + final OfferBookViewModel model = new BtcOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, + null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); assertEquals(7, model.maxPlacesForPrice.intValue()); - offerBookListItems.addAll(make(btcBuyItem.but(with(price, 149558240L)))); //14955.8240 + offerBookListItems.addAll(make(xmrBuyItem.but(with(price, 149558240L)))); //14955.8240 assertEquals(10, model.maxPlacesForPrice.intValue()); - offerBookListItems.addAll(make(btcBuyItem.but(with(price, 14955824L)))); //1495.58240 + offerBookListItems.addAll(make(xmrBuyItem.but(with(price, 14955824L)))); //1495.58240 assertEquals(10, model.maxPlacesForPrice.intValue()); } @@ -370,8 +376,8 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, - null, null, null, getPriceUtil(), null, coinFormatter); + final OfferBookViewModel model = new BtcOfferBookViewModel(null, null, offerBook, empty, null, null, null, + null, null, null, getPriceUtil(), null, coinFormatter, null); assertEquals(0, model.maxPlacesForMarketPriceMargin.intValue()); } @@ -382,24 +388,31 @@ public class OfferBookViewModelTest { PriceFeedService priceFeedService = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); - final Maker item = btcBuyItem.but(with(useMarketBasedPrice, true)); + final Maker item = xmrBuyItem.but(with(useMarketBasedPrice, true)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(priceFeedService.getMarketPrice(anyString())).thenReturn(null); when(priceFeedService.updateCounterProperty()).thenReturn(new SimpleIntegerProperty()); final OfferBookListItem item1 = make(item); + assertNotNull(item1.getHashOfPayload()); item1.getOffer().setPriceFeedService(priceFeedService); + final OfferBookListItem item2 = make(item.but(with(marketPriceMargin, 0.0197))); + assertNotNull(item2.getHashOfPayload()); item2.getOffer().setPriceFeedService(priceFeedService); + final OfferBookListItem item3 = make(item.but(with(marketPriceMargin, 0.1))); + assertNotNull(item3.getHashOfPayload()); item3.getOffer().setPriceFeedService(priceFeedService); + final OfferBookListItem item4 = make(item.but(with(marketPriceMargin, -0.1))); + assertNotNull(item4.getHashOfPayload()); item4.getOffer().setPriceFeedService(priceFeedService); offerBookListItems.addAll(item1, item2); - final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, priceFeedService, - null, null, null, getPriceUtil(), null, coinFormatter); + final OfferBookViewModel model = new BtcOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, priceFeedService, + null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); assertEquals(8, model.maxPlacesForMarketPriceMargin.intValue()); //" (1.97%)" @@ -419,18 +432,21 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true)); - final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, - null, null, null, getPriceUtil(), null, coinFormatter); + final OfferBookViewModel model = new BtcOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, + null, null, null, getPriceUtil(), null, coinFormatter, null); - final OfferBookListItem item = make(btcBuyItem.but( + final OfferBookListItem item = make(xmrBuyItem.but( with(useMarketBasedPrice, true), with(marketPriceMargin, -0.12))); + assertNotNull(item.getHashOfPayload()); - final OfferBookListItem lowItem = make(btcBuyItem.but( + final OfferBookListItem lowItem = make(xmrBuyItem.but( with(useMarketBasedPrice, true), with(marketPriceMargin, 0.01))); + assertNotNull(lowItem.getHashOfPayload()); - final OfferBookListItem fixedItem = make(btcBuyItem); + final OfferBookListItem fixedItem = make(xmrBuyItem); + assertNotNull(fixedItem.getHashOfPayload()); item.getOffer().setPriceFeedService(priceFeedService); lowItem.getOffer().setPriceFeedService(priceFeedService); @@ -592,7 +608,7 @@ public class OfferBookViewModelTest { false, 0, 0, - "XMR", + "BTC", tradeCurrencyCode, paymentMethodId, null, @@ -616,9 +632,10 @@ public class OfferBookViewModelTest { false, null, null, - 1, + 0, null, null, null)); } } + diff --git a/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java b/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java deleted file mode 100644 index 2bec39d61d..0000000000 --- a/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java +++ /dev/null @@ -1,127 +0,0 @@ -package bisq.desktop.main.portfolio.editoffer; - -import bisq.desktop.util.validation.SecurityDepositValidator; - -import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.btc.model.XmrAddressEntry; -import bisq.core.btc.wallet.XmrWalletService; -import bisq.core.locale.Country; -import bisq.core.locale.CryptoCurrency; -import bisq.core.locale.GlobalSettings; -import bisq.core.locale.Res; -import bisq.core.offer.CreateOfferService; -import bisq.core.offer.OfferPayload; -import bisq.core.offer.OfferUtil; -import bisq.core.offer.OpenOffer; -import bisq.core.payment.CryptoCurrencyAccount; -import bisq.core.payment.PaymentAccount; -import bisq.core.provider.fee.FeeService; -import bisq.core.provider.price.MarketPrice; -import bisq.core.provider.price.PriceFeedService; -import bisq.core.trade.statistics.TradeStatisticsManager; -import bisq.core.user.Preferences; -import bisq.core.user.User; -import bisq.core.util.validation.InputValidator; - -import org.bitcoinj.core.Coin; - -import javafx.beans.property.SimpleIntegerProperty; - -import javafx.collections.FXCollections; - -import java.time.Instant; - -import java.util.UUID; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import static bisq.desktop.maker.OfferMaker.btcBCHCOffer; -import static bisq.desktop.maker.PreferenceMakers.empty; -import static com.natpryce.makeiteasy.MakeItEasy.make; -import static org.junit.Assert.assertNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class EditOfferDataModelTest { - - private EditOfferDataModel model; - private User user; - - @Rule - public final ExpectedException exception = ExpectedException.none(); - - @Before - public void setUp() { - - final CryptoCurrency xmr = new CryptoCurrency("XMR", "monero"); - GlobalSettings.setDefaultTradeCurrency(xmr); - Res.setup(); - - FeeService feeService = mock(FeeService.class); - XmrAddressEntry addressEntry = mock(XmrAddressEntry.class); - XmrWalletService xmrWalletService = mock(XmrWalletService.class); - PriceFeedService priceFeedService = mock(PriceFeedService.class); - user = mock(User.class); - PaymentAccount paymentAccount = mock(PaymentAccount.class); - Preferences preferences = mock(Preferences.class); - SecurityDepositValidator securityDepositValidator = mock(SecurityDepositValidator.class); - AccountAgeWitnessService accountAgeWitnessService = mock(AccountAgeWitnessService.class); - CreateOfferService createOfferService = mock(CreateOfferService.class); - OfferUtil offerUtil = mock(OfferUtil.class); - - when(xmrWalletService.getOrCreateAddressEntry(anyString(), any())).thenReturn(addressEntry); - when(xmrWalletService.getBalanceForSubaddress(any(Integer.class))).thenReturn(Coin.valueOf(1000L)); - when(priceFeedService.updateCounterProperty()).thenReturn(new SimpleIntegerProperty()); - when(priceFeedService.getMarketPrice(anyString())).thenReturn( - new MarketPrice("USD", - 12684.0450, - Instant.now().getEpochSecond(), - true)); - when(feeService.getTxFee(anyInt())).thenReturn(Coin.valueOf(1000L)); - when(user.findFirstPaymentAccountWithCurrency(any())).thenReturn(paymentAccount); - when(user.getPaymentAccountsAsObservable()).thenReturn(FXCollections.observableSet()); - when(securityDepositValidator.validate(any())).thenReturn(new InputValidator.ValidationResult(false)); - when(accountAgeWitnessService.getMyTradeLimit(any(), any(), any())).thenReturn(100000000L); - when(preferences.getUserCountry()).thenReturn(new Country("US", "United States", null)); - when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString()); - - model = new EditOfferDataModel(createOfferService, - null, - offerUtil, - xmrWalletService, - empty, - user, - null, - priceFeedService, - accountAgeWitnessService, - feeService, - null, - null, - mock(TradeStatisticsManager.class), - null); - } - - @Test - public void testEditOfferOfRemovedAsset() { - - final CryptoCurrencyAccount bitcoinClashicAccount = new CryptoCurrencyAccount(); - bitcoinClashicAccount.setId("BCHC"); - - when(user.getPaymentAccount(anyString())).thenReturn(bitcoinClashicAccount); - - model.applyOpenOffer(new OpenOffer(make(btcBCHCOffer))); - assertNull(model.getPreselectedPaymentAccount()); - } - - @Test - public void testInitializeEditOfferWithRemovedAsset() { - exception.expect(IllegalArgumentException.class); - model.initWithData(OfferPayload.Direction.BUY, null); - } -} diff --git a/desktop/src/test/java/bisq/desktop/maker/OfferMaker.java b/desktop/src/test/java/bisq/desktop/maker/OfferMaker.java index 28a0c8f14c..9c4d0a7f03 100644 --- a/desktop/src/test/java/bisq/desktop/maker/OfferMaker.java +++ b/desktop/src/test/java/bisq/desktop/maker/OfferMaker.java @@ -18,33 +18,68 @@ package bisq.desktop.maker; import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferPayload; +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.Encryption; +import bisq.common.crypto.PubKeyRing; +import bisq.common.crypto.Sig; + +import java.net.UnknownHostException; + +import java.util.ArrayList; +import java.util.List; + import com.natpryce.makeiteasy.Instantiator; import com.natpryce.makeiteasy.Maker; import com.natpryce.makeiteasy.Property; import static com.natpryce.makeiteasy.MakeItEasy.a; import static com.natpryce.makeiteasy.MakeItEasy.with; +import static com.natpryce.makeiteasy.Property.newProperty; +import static java.lang.System.currentTimeMillis; +import static java.net.InetAddress.getLocalHost; +@SuppressWarnings("InstantiationOfUtilityClass") public class OfferMaker { - public static final Property price = new Property<>(); - public static final Property minAmount = new Property<>(); - public static final Property amount = new Property<>(); - public static final Property baseCurrencyCode = new Property<>(); - public static final Property counterCurrencyCode = new Property<>(); - public static final Property direction = new Property<>(); - public static final Property useMarketBasedPrice = new Property<>(); - public static final Property marketPriceMargin = new Property<>(); - public static final Property id = new Property<>(); + public static final Property id = newProperty(); + public static final Property paymentMethodId = newProperty(); + public static final Property paymentAccountId = newProperty(); + public static final Property offerFeePaymentTxId = newProperty(); + public static final Property countryCode = newProperty(); + public static final Property> countryCodes = newProperty(); + public static final Property date = newProperty(); + public static final Property price = newProperty(); + public static final Property minAmount = newProperty(); + public static final Property amount = newProperty(); + public static final Property baseCurrencyCode = newProperty(); + public static final Property counterCurrencyCode = newProperty(); + public static final Property direction = newProperty(); + public static final Property useMarketBasedPrice = newProperty(); + public static final Property marketPriceMargin = newProperty(); + public static final Property nodeAddress = newProperty(); + public static final Property> nodeAddresses = newProperty(); + public static final Property pubKeyRing = newProperty(); + public static final Property blockHeight = newProperty(); + public static final Property txFee = newProperty(); + public static final Property makerFee = newProperty(); + public static final Property buyerSecurityDeposit = newProperty(); + public static final Property sellerSecurityDeposit = newProperty(); + public static final Property tradeLimit = newProperty(); + public static final Property maxTradePeriod = newProperty(); + public static final Property lowerClosePrice = newProperty(); + public static final Property upperClosePrice = newProperty(); + public static final Property protocolVersion = newProperty(); public static final Instantiator Offer = lookup -> new Offer( new OfferPayload(lookup.valueOf(id, "1234"), - 0L, - null, - null, - lookup.valueOf(direction, OfferPayload.Direction.BUY), + lookup.valueOf(date, currentTimeMillis()), + lookup.valueOf(nodeAddress, getLocalHostNodeWithPort(10000)), + lookup.valueOf(pubKeyRing, genPubKeyRing()), + lookup.valueOf(direction, OfferDirection.BUY), lookup.valueOf(price, 100000L), lookup.valueOf(marketPriceMargin, 0.0), lookup.valueOf(useMarketBasedPrice, false), @@ -52,33 +87,47 @@ public class OfferMaker { lookup.valueOf(minAmount, 100000L), lookup.valueOf(baseCurrencyCode, "XMR"), lookup.valueOf(counterCurrencyCode, "USD"), - "SEPA", - "", + lookup.valueOf(paymentMethodId, "SEPA"), + lookup.valueOf(paymentAccountId, "00002c4d-1ffc-4208-8ff3-e669817b0000"), + lookup.valueOf(offerFeePaymentTxId, "0000dcd1d388b95714c96ce13f5cb000090c41a1faf89e4ce7680938cc170000"), + lookup.valueOf(countryCode, "FR"), + lookup.valueOf(countryCodes, new ArrayList<>() {{ + add("FR"); + }}), null, null, - null, - null, - null, - "", - 0L, - 0L, - 0L, - 0L, - 0L, - 0L, - 0L, + "2", + lookup.valueOf(blockHeight, 700000L), + lookup.valueOf(txFee, 250L), + lookup.valueOf(makerFee, 1000L), + lookup.valueOf(buyerSecurityDeposit, 10000L), + lookup.valueOf(sellerSecurityDeposit, 10000L), + lookup.valueOf(tradeLimit, 0L), + lookup.valueOf(maxTradePeriod, 0L), false, false, - 0L, - 0L, + lookup.valueOf(lowerClosePrice, 0L), + lookup.valueOf(upperClosePrice, 0L), false, null, null, - 0, - null, + lookup.valueOf(protocolVersion, 0), + getLocalHostNodeWithPort(99999), null, null)); - public static final Maker btcUsdOffer = a(Offer); + public static final Maker xmrUsdOffer = a(Offer); public static final Maker btcBCHCOffer = a(Offer).but(with(counterCurrencyCode, "BCHC")); + + static NodeAddress getLocalHostNodeWithPort(int port) { + try { + return new NodeAddress(getLocalHost().getHostAddress(), port); + } catch (UnknownHostException ex) { + throw new IllegalStateException(ex); + } + } + + static PubKeyRing genPubKeyRing() { + return new PubKeyRing(Sig.generateKeyPair().getPublic(), Encryption.generateKeyPair().getPublic()); + } } diff --git a/desktop/src/test/java/bisq/desktop/maker/PreferenceMakers.java b/desktop/src/test/java/bisq/desktop/maker/PreferenceMakers.java index a60c1941de..412d86c92c 100644 --- a/desktop/src/test/java/bisq/desktop/maker/PreferenceMakers.java +++ b/desktop/src/test/java/bisq/desktop/maker/PreferenceMakers.java @@ -45,7 +45,8 @@ public class PreferenceMakers { lookup.valueOf(config, new SameValueDonor(null)), lookup.valueOf(feeService, new SameValueDonor(null)), lookup.valueOf(localBitcoinNode, new SameValueDonor(null)), - lookup.valueOf(useTorFlagFromOptions, new SameValueDonor(null))); + lookup.valueOf(useTorFlagFromOptions, new SameValueDonor(null)) + ); public static final Preferences empty = make(a(Preferences)); diff --git a/desktop/src/test/java/bisq/desktop/util/DisplayUtilsTest.java b/desktop/src/test/java/bisq/desktop/util/DisplayUtilsTest.java index 41308dd47c..63d73c106e 100644 --- a/desktop/src/test/java/bisq/desktop/util/DisplayUtilsTest.java +++ b/desktop/src/test/java/bisq/desktop/util/DisplayUtilsTest.java @@ -1,11 +1,13 @@ package bisq.desktop.util; +import bisq.core.locale.GlobalSettings; import bisq.core.locale.Res; import bisq.core.monetary.Volume; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; -import bisq.core.util.coin.ImmutableCoinFormatter; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.coin.ImmutableCoinFormatter; import bisq.common.config.Config; @@ -17,7 +19,7 @@ import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; -import static bisq.desktop.maker.OfferMaker.btcUsdOffer; +import static bisq.desktop.maker.OfferMaker.xmrUsdOffer; import static bisq.desktop.maker.VolumeMaker.usdVolume; import static bisq.desktop.maker.VolumeMaker.volumeString; import static com.natpryce.makeiteasy.MakeItEasy.make; @@ -31,7 +33,8 @@ public class DisplayUtilsTest { @Before public void setUp() { - Locale.setDefault(new Locale("en", "US")); + Locale.setDefault(Locale.US); + GlobalSettings.setLocale(Locale.US); Res.setBaseCurrencyCode("XMR"); Res.setBaseCurrencyName("Monero"); } @@ -49,9 +52,9 @@ public class DisplayUtilsTest { @Test public void testFormatVolume() { - assertEquals("1", DisplayUtils.formatVolume(make(btcUsdOffer), true, 4)); - assertEquals("100", DisplayUtils.formatVolume(make(usdVolume))); - assertEquals("1775", DisplayUtils.formatVolume(make(usdVolume.but(with(volumeString, "1774.62"))))); + assertEquals("1", VolumeUtil.formatVolume(make(xmrUsdOffer), true, 4)); + assertEquals("100", VolumeUtil.formatVolume(make(usdVolume))); + assertEquals("1775", VolumeUtil.formatVolume(make(usdVolume.but(with(volumeString, "1774.62"))))); } @Test @@ -61,7 +64,7 @@ public class DisplayUtilsTest { when(offer.getMinVolume()).thenReturn(xmr); when(offer.getVolume()).thenReturn(xmr); - assertEquals("0.10000000", DisplayUtils.formatVolume(offer.getVolume())); + assertEquals("0.10000000", VolumeUtil.formatVolume(offer.getVolume())); } @Test @@ -73,7 +76,7 @@ public class DisplayUtilsTest { when(offer.getMinVolume()).thenReturn(xmrMin); when(offer.getVolume()).thenReturn(xmrMax); - assertEquals("0.10000000 - 0.25000000", DisplayUtils.formatVolume(offer, false, 0)); + assertEquals("0.10000000 - 0.25000000", VolumeUtil.formatVolume(offer, false, 0)); } @Test @@ -82,7 +85,7 @@ public class DisplayUtilsTest { when(offer.getMinVolume()).thenReturn(null); when(offer.getVolume()).thenReturn(null); - assertEquals("", DisplayUtils.formatVolume(offer.getVolume())); + assertEquals("", VolumeUtil.formatVolume(offer.getVolume())); } @Test diff --git a/desktop/src/test/java/bisq/desktop/util/GUIUtilTest.java b/desktop/src/test/java/bisq/desktop/util/GUIUtilTest.java index c4d17c2ffd..9b9a8a3c69 100644 --- a/desktop/src/test/java/bisq/desktop/util/GUIUtilTest.java +++ b/desktop/src/test/java/bisq/desktop/util/GUIUtilTest.java @@ -56,8 +56,8 @@ public class GUIUtilTest { public void setup() { Locale.setDefault(new Locale("en", "US")); GlobalSettings.setLocale(new Locale("en", "US")); - Res.setBaseCurrencyCode("XMR"); - Res.setBaseCurrencyName("Monero"); + Res.setBaseCurrencyCode("BTC"); + Res.setBaseCurrencyName("Bitcoin"); } @Test diff --git a/desktop/src/test/java/bisq/desktop/util/validation/CapitualValidatorTest.java b/desktop/src/test/java/bisq/desktop/util/validation/CapitualValidatorTest.java new file mode 100644 index 0000000000..36260dfdff --- /dev/null +++ b/desktop/src/test/java/bisq/desktop/util/validation/CapitualValidatorTest.java @@ -0,0 +1,45 @@ +package bisq.desktop.util.validation; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.util.validation.RegexValidator; + +import bisq.common.config.BaseCurrencyNetwork; +import bisq.common.config.Config; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CapitualValidatorTest { + @Before + public void setup() { + final BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); + final String currencyCode = baseCurrencyNetwork.getCurrencyCode(); + Res.setBaseCurrencyCode(currencyCode); + Res.setBaseCurrencyName(baseCurrencyNetwork.getCurrencyName()); + CurrencyUtil.setBaseCurrencyCode(currencyCode); + } + + @Test + public void validate() { + CapitualValidator validator = new CapitualValidator( + new RegexValidator() + ); + + assertTrue(validator.validate("CAP-123456").isValid); + assertTrue(validator.validate("CAP-XXXXXX").isValid); + assertTrue(validator.validate("CAP-123XXX").isValid); + + assertFalse(validator.validate("").isValid); + assertFalse(validator.validate(null).isValid); + assertFalse(validator.validate("123456").isValid); + assertFalse(validator.validate("XXXXXX").isValid); + assertFalse(validator.validate("123XXX").isValid); + assertFalse(validator.validate("12XXX").isValid); + assertFalse(validator.validate("CAP-12XXX").isValid); + assertFalse(validator.validate("CA-12XXXx").isValid); + } +} diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index e190bfbc5a..ea2e76b7d1 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1324,6 +1324,11 @@ + + + + + diff --git a/inventory/src/main/java/bisq/inventory/InventoryMonitor.java b/inventory/src/main/java/bisq/inventory/InventoryMonitor.java index 086bd6a2d6..6b77c136a3 100644 --- a/inventory/src/main/java/bisq/inventory/InventoryMonitor.java +++ b/inventory/src/main/java/bisq/inventory/InventoryMonitor.java @@ -26,7 +26,7 @@ import bisq.core.network.p2p.inventory.model.InventoryItem; import bisq.core.network.p2p.inventory.model.RequestInfo; import bisq.core.network.p2p.seed.DefaultSeedNodeRepository; import bisq.core.proto.network.CoreNetworkProtoResolver; - +import bisq.core.util.JsonUtil; import bisq.network.p2p.NetworkNodeProvider; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.network.NetworkNode; @@ -236,7 +236,7 @@ public class InventoryMonitor implements SetupListener { inventoryWebServer.onNewRequestInfo(requestInfoListByNode, requestCounter); - String json = Utilities.objectToJson(requestInfo); + String json = JsonUtil.objectToJson(requestInfo); jsonFileManagerByNodeAddress.get(nodeAddress).writeToDisc(json, String.valueOf(requestInfo.getRequestStartTime())); } diff --git a/p2p/src/main/java/bisq/network/utils/Utils.java b/p2p/src/main/java/bisq/network/utils/Utils.java index b9deace93b..1bc875d093 100644 --- a/p2p/src/main/java/bisq/network/utils/Utils.java +++ b/p2p/src/main/java/bisq/network/utils/Utils.java @@ -34,4 +34,8 @@ public class Utils { return new Random().nextInt(10000) + 50000; } } + + public static boolean isV3Address(String address) { + return address.matches("[a-z2-7]{56}.onion"); + } } diff --git a/pricenode/src/main/java/bisq/price/Main.java b/pricenode/src/main/java/bisq/price/Main.java index a4663a5b48..3e83fc497f 100644 --- a/pricenode/src/main/java/bisq/price/Main.java +++ b/pricenode/src/main/java/bisq/price/Main.java @@ -38,7 +38,7 @@ public class Main { private static Properties bisqProperties() { Properties props = new Properties(); - File propsFile = new File(System.getenv("HOME"), ".config/bisq.properties"); + File propsFile = new File(System.getenv("HOME"), ".config/haveno.properties"); if (propsFile.exists()) { try { props.load(new FileInputStream(propsFile)); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 4eb0767c37..dff649ab7c 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -482,10 +482,10 @@ message CreateOfferRequest { string direction = 2; string price = 3; bool use_market_based_price = 4; - double market_price_margin = 5; + double market_price_margin_pct = 5; uint64 amount = 6 [jstype = JS_STRING]; uint64 min_amount = 7 [jstype = JS_STRING]; - double buyer_security_deposit = 8; + double buyer_security_deposit_pct = 8; string trigger_price = 9; string payment_account_id = 10; } @@ -504,15 +504,15 @@ message CancelOfferReply { message OfferInfo { string id = 1; string direction = 2; - uint64 price = 3; + string price = 3; bool use_market_based_price = 4; - double market_price_margin = 5; + double market_price_margin_pct = 5; uint64 amount = 6; uint64 min_amount = 7; - uint64 volume = 8; - uint64 min_volume = 9; + string volume = 8; + string min_volume = 9; uint64 buyer_security_deposit = 10; - uint64 trigger_price = 11; + string trigger_price = 11; string payment_account_id = 12; string payment_method_id = 13; string payment_method_short_name = 14; @@ -524,6 +524,12 @@ message OfferInfo { string offer_fee_payment_tx_id = 20; uint64 tx_fee = 21; uint64 maker_fee = 22; + bool is_activated = 23; + bool is_my_offer = 24; + string owner_node_address = 25; + string pub_key_ring = 26; + string version_nr = 27; + int32 protocol_version = 28; } message AvailabilityResultWithDescription { @@ -736,6 +742,13 @@ message GetTradeReply { } message GetTradesRequest { + // Rpc method GetTrades parameter determining what category of trade list is is being requested. + enum Category { + OPEN = 0; // Get all currently open trades. + CLOSED = 1; // Get all completed trades. + FAILED = 2; // Get all failed trades. + } + Category category = 1; } message GetTradesReply { @@ -785,20 +798,21 @@ message TradeInfo { string taker_fee_tx_id = 9; reserved 10; // was depositTxId string payout_tx_id = 11; - uint64 trade_amount_as_long = 12; - uint64 trade_price = 13; + uint64 amount_as_long = 12; + string price = 13; string trading_peer_node_address = 14; string state = 15; string phase = 16; - string trade_period_state = 17; + string period_state = 17; bool is_deposit_published = 18; bool is_deposit_unlocked = 19; bool is_payment_sent = 20; bool is_payment_received = 21; bool is_payout_published = 22; - bool is_withdrawn = 23; + bool is_completed = 23; string contract_as_json = 24; ContractInfo contract = 25; + string trade_volume = 26; string maker_deposit_tx_id = 100; string taker_deposit_tx_id = 101; @@ -825,6 +839,7 @@ message PaymentAccountPayloadInfo { string id = 1; string payment_method_id = 2; string address = 3; + string payment_details = 4; } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index f1d721ce3c..e5c6873e09 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -117,8 +117,6 @@ message GetUpdatedDataRequest { string version = 4; } -// peers - message GetPeersRequest { NodeAddress sender_node_address = 1; int32 nonce = 2; @@ -141,8 +139,6 @@ message Pong { int32 request_nonce = 1; } -// Inventory - message GetInventoryRequest { string version = 1; } @@ -151,8 +147,6 @@ message GetInventoryResponse { map inventory = 1; } -// offer - message SignOfferRequest { string offer_id = 1; NodeAddress sender_node_address = 2; @@ -200,8 +194,6 @@ message RefreshOfferMessage { int32 sequence_number = 4; } -// storage - message AddDataMessage { StorageEntryWrapper entry = 1; } @@ -218,8 +210,6 @@ message AddPersistableNetworkPayloadMessage { PersistableNetworkPayload payload = 1; } -// misc - message CloseConnectionMessage { string reason = 1; } @@ -242,8 +232,6 @@ message PrefixedSealedAndSignedMessage { string uid = 4; } -// trade - message InputsForDepositTxRequest { string trade_id = 1; NodeAddress sender_node_address = 2; @@ -779,7 +767,7 @@ message Filter { message TradeStatistics2 { string base_currency = 1 [deprecated = true]; string counter_currency = 2 [deprecated = true]; - OfferPayload.Direction direction = 3 [deprecated = true]; + OfferDirection direction = 3 [deprecated = true]; int64 trade_price = 4 [deprecated = true]; int64 trade_amount = 5 [deprecated = true]; int64 trade_date = 6 [deprecated = true]; @@ -821,19 +809,13 @@ message MailboxStoragePayload { } message OfferPayload { - enum Direction { - PB_ERROR = 0; - BUY = 1; - SELL = 2; - } - string id = 1; int64 date = 2; NodeAddress owner_node_address = 3; PubKeyRing pub_key_ring = 4; - Direction direction = 5; + OfferDirection direction = 5; int64 price = 6; - double market_price_margin = 7; + double market_price_margin_pct = 7; bool use_market_based_price = 8; int64 amount = 9; int64 min_amount = 10; @@ -868,6 +850,12 @@ message OfferPayload { repeated string reserve_tx_key_images = 1003; } +enum OfferDirection { + OFFER_DIRECTION_ERROR = 0; + BUY = 1; + SELL = 2; +} + message AccountAgeWitness { bytes hash = 1; int64 date = 2; @@ -1075,6 +1063,13 @@ message PaymentAccountPayload { AustraliaPayidPayload australia_payid_payload = 30; AmazonGiftCardAccountPayload amazon_gift_card_account_payload = 31; CashByMailAccountPayload cash_by_mail_account_payload = 32; + CapitualAccountPayload capitual_account_payload = 33; + PayseraAccountPayload Paysera_account_payload = 34; + PaxumAccountPayload Paxum_account_payload = 35; + SwiftAccountPayload swift_account_payload = 36; + CelPayAccountPayload cel_pay_account_payload = 37; + MoneseAccountPayload monese_account_payload = 38; + VerseAccountPayload verse_account_payload = 39; } map exclude_from_json_data = 15; } @@ -1106,6 +1101,16 @@ message CountryBasedPaymentAccountPayload { WesternUnionAccountPayload western_union_account_payload = 5; SepaInstantAccountPayload sepa_instant_account_payload = 6; F2FAccountPayload f2f_account_payload = 7; + UpiAccountPayload upi_account_payload = 9; + PaytmAccountPayload paytm_account_payload = 10; + IfscBasedAccountPayload ifsc_based_account_payload = 11; + NequiAccountPayload nequi_account_payload = 12; + BizumAccountPayload bizum_account_payload = 13; + PixAccountPayload pix_account_payload = 14; + SatispayAccountPayload satispay_account_payload = 15; + StrikeAccountPayload strike_account_payload = 16; + TikkieAccountPayload tikkie_account_payload = 17; + TransferwiseUsdAccountPayload transferwise_usd_account_payload = 18; } } @@ -1122,10 +1127,20 @@ message BankAccountPayload { NationalBankAccountPayload national_bank_account_payload = 9; SameBankAccountPayload same_bank_accont_payload = 10; SpecificBanksAccountPayload specific_banks_account_payload = 11; + AchTransferAccountPayload ach_transfer_account_payload = 13; + DomesticWireTransferAccountPayload domestic_wire_transfer_account_payload = 14; } string national_account_id = 12; } +message AchTransferAccountPayload { + string holder_address = 1; +} + +message DomesticWireTransferAccountPayload { + string holder_address = 1; +} + message NationalBankAccountPayload { } @@ -1230,6 +1245,7 @@ message OKPayAccountPayload { message UpholdAccountPayload { string account_id = 1; + string account_owner = 2; } // Deprecated, not used anymore @@ -1277,6 +1293,34 @@ message F2FAccountPayload { string extra_info = 3; } +message IfscBasedAccountPayload { + string holder_name = 1; + string account_nr = 2; + string ifsc = 3; + oneof message { + NeftAccountPayload neft_account_payload = 4; + RtgsAccountPayload rtgs_account_payload = 5; + ImpsAccountPayload imps_account_payload = 6; + } +} + +message NeftAccountPayload { +} + +message RtgsAccountPayload { +} + +message ImpsAccountPayload { +} + +message UpiAccountPayload { + string virtual_payment_address = 1; +} + +message PaytmAccountPayload { + string email_or_mobile_nr = 1; +} + message CashByMailAccountPayload { string postal_address = 1; string contact = 2; @@ -1295,11 +1339,83 @@ message TransferwiseAccountPayload { string email = 1; } -/////////////////////////////////////////////////////////////////////////////////////////// -// PersistableEnvelope -/////////////////////////////////////////////////////////////////////////////////////////// +message TransferwiseUsdAccountPayload { + string email = 1; + string holder_name = 2; + string beneficiary_address = 3; +} + +message PayseraAccountPayload { + string email = 1; +} + +message PaxumAccountPayload { + string email = 1; +} + +message CapitualAccountPayload { + string account_nr = 1; +} + +message CelPayAccountPayload { + string email = 1; +} + +message NequiAccountPayload { + string mobile_nr = 1; +} + +message BizumAccountPayload { + string mobile_nr = 1; +} + +message PixAccountPayload { + string pix_key = 1; +} + +message MoneseAccountPayload { + string mobile_nr = 1; + string holder_name = 2; +} + +message SatispayAccountPayload { + string mobile_nr = 1; + string holder_name = 2; +} + +message StrikeAccountPayload { + string holder_name = 1; +} + +message TikkieAccountPayload { + string iban = 1; +} + +message VerseAccountPayload { + string holder_name = 1; +} + +message SwiftAccountPayload { + string beneficiary_name = 1; + string beneficiary_account_nr = 2; + string beneficiary_address = 3; + string beneficiary_city = 4; + string beneficiary_phone = 5; + string special_instructions = 6; + + string bank_swift_code = 7; + string bank_country_code = 8; + string bank_name = 9; + string bank_branch = 10; + string bank_address = 11; + + string intermediary_swift_code = 12; + string intermediary_country_code = 13; + string intermediary_name = 14; + string intermediary_branch = 15; + string intermediary_address = 16; +} -// Those are persisted to disc message PersistableEnvelope { oneof message { SequenceNumberMap sequence_number_map = 1; @@ -1309,7 +1425,9 @@ message PersistableEnvelope { NavigationPath navigation_path = 5; TradableList tradable_list = 6; - // TradeStatisticsList trade_statistics_list = 7; // Was used in pre v0.6.0 version. Not used anymore. + + // TradeStatisticsList trade_statistics_list = 7; Deprecated, Was used in pre v0.6.0 version. Not used anymore. + ArbitrationDisputeList arbitration_dispute_list = 8; PreferencesPayload preferences_payload = 9; @@ -1335,10 +1453,6 @@ message PersistableEnvelope { } } -/////////////////////////////////////////////////////////////////////////////////////////// -// Collections -/////////////////////////////////////////////////////////////////////////////////////////// - message SequenceNumberMap { repeated SequenceNumberEntry sequence_number_entries = 1; } @@ -1588,14 +1702,14 @@ message Trade { string taker_fee_tx_id = 3; reserved 4; string payout_tx_id = 5; - int64 trade_amount_as_long = 6; + int64 amount_as_long = 6; int64 tx_fee_as_long = 7; int64 taker_fee_as_long = 8; int64 take_offer_date = 9; - int64 trade_price = 10; + int64 price = 10; State state = 11; DisputeState dispute_state = 12; - TradePeriodState trade_period_state = 13; + TradePeriodState period_state = 13; Contract contract = 14; string contract_as_json = 15; bytes contract_hash = 16; @@ -1831,11 +1945,15 @@ message PreferencesPayload { int32 css_theme = 51; bool tac_accepted_v120 = 52; repeated AutoConfirmSettings auto_confirm_settings = 53; - bool hide_non_account_payment_methods = 54; - bool show_offers_matching_my_accounts = 55; - bool deny_api_taker = 56; - bool notify_on_pre_release = 57; - MoneroNodeSettings monero_node_settings = 58; + double bsq_average_trim_threshold = 54; + bool hide_non_account_payment_methods = 55; + bool show_offers_matching_my_accounts = 56; + bool deny_api_taker = 57; + bool notify_on_pre_release = 58; + MoneroNodeSettings monero_node_settings = 59; + int32 clear_data_after_days = 60; + string buy_screen_crypto_currency_code = 61; + string sell_screen_crypto_currency_code = 62; } message AutoConfirmSettings { @@ -1875,10 +1993,6 @@ message UserPayload { map cookie = 16; } -/////////////////////////////////////////////////////////////////////////////////////////// -// Misc -/////////////////////////////////////////////////////////////////////////////////////////// - message BlockChainExplorer { string name = 1; string tx_url = 2; @@ -1902,8 +2016,6 @@ message PaymentMethod { repeated string supported_asset_codes = 4; } -// Currency - message Currency { string currency_code = 1; } @@ -1936,10 +2048,6 @@ message Region { string name = 2; } -/////////////////////////////////////////////////////////////////////////////////////////// -// Notifications -/////////////////////////////////////////////////////////////////////////////////////////// - message PriceAlertFilter { string currencyCode = 1; int64 high = 2; @@ -1953,10 +2061,6 @@ message MarketAlertFilter { repeated string alert_ids = 4; } -/////////////////////////////////////////////////////////////////////////////////////////// -// Mock -/////////////////////////////////////////////////////////////////////////////////////////// - message MockMailboxPayload { string message = 1; NodeAddress sender_node_address = 2;