diff --git a/apitest/docs/api-beta-test-guide.md b/apitest/docs/api-beta-test-guide.md index 8c1b41112bc..0fb5f2aa751 100644 --- a/apitest/docs/api-beta-test-guide.md +++ b/apitest/docs/api-beta-test-guide.md @@ -17,7 +17,7 @@ option adjustments to compensate. **Shell**: Bash -**Java SDK**: Version 10, 11, or 12 +**Java SDK**: Version 11-15 (source target = 10) **Bitcoin-Core**: Version 0.19, 0.20, or 0.21 diff --git a/apitest/scripts/trade-simulation-env.sh b/apitest/scripts/trade-simulation-env.sh index 45fc385e072..7407426f980 100755 --- a/apitest/scripts/trade-simulation-env.sh +++ b/apitest/scripts/trade-simulation-env.sh @@ -179,6 +179,51 @@ parselimitorderopts() { fi } +parsexmrscriptopts() { + usage() { + echo "Usage: $0 [-d buy|sell] [-f || -m ] [-a ]" 1>&2 + exit 1; + } + + local OPTIND o d f m a + while getopts "d:f:m:a:" o; do + case "${o}" in + d) d=$(echo "${OPTARG}" | tr '[:lower:]' '[:upper:]') + ((d == "BUY" || d == "SELL")) || usage + export DIRECTION=${d} + ;; + f) f=${OPTARG} + export FIXED_PRICE=${f} + ;; + m) m=${OPTARG} + export MKT_PRICE_MARGIN=${m} + ;; + a) a=${OPTARG} + export AMOUNT=${a} + ;; + *) usage ;; + esac + done + shift $((OPTIND-1)) + + if [ -z "${d}" ] || [ -z "${a}" ]; then + usage + fi + + if [ -z "${f}" ] && [ -z "${m}" ]; then + usage + fi + + if [ "$DIRECTION" = "SELL" ] + then + export BOB_ROLE="(taker/buyer)" + export ALICE_ROLE="(maker/seller)" + else + export BOB_ROLE="(taker/seller)" + export ALICE_ROLE="(maker/buyer)" + fi +} + checkbitcoindrunning() { # There may be a '+' char in the path and we have to escape it for pgrep. if [[ $APP_HOME == *"+"* ]]; then diff --git a/apitest/scripts/trade-simulation-utils.sh b/apitest/scripts/trade-simulation-utils.sh index c43beaf2ec8..0e6a770626a 100755 --- a/apitest/scripts/trade-simulation-utils.sh +++ b/apitest/scripts/trade-simulation-utils.sh @@ -368,7 +368,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 +407,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 +427,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 +446,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" @@ -532,7 +532,7 @@ executetrade() { fi # Generate some btc blocks - printdate "Generating btc blocks after fiat transfer." + printdate "Generating btc blocks after funds transfer." genbtcblocks 2 2 printbreak diff --git a/apitest/scripts/trade-simulation.sh b/apitest/scripts/trade-simulation.sh index 5aa540bf71a..8fa12d4a553 100755 --- a/apitest/scripts/trade-simulation.sh +++ b/apitest/scripts/trade-simulation.sh @@ -7,7 +7,7 @@ # # 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 10), and bitcoin-core (v0.19, v0.20, or v0.21). # # - Bisq must be fully built with apitest dao setup files installed. # Build command: `./gradlew clean build :apitest:installDaoSetup` @@ -26,7 +26,7 @@ # # `$ 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: # diff --git a/apitest/scripts/trade-xmr-simulation.sh b/apitest/scripts/trade-xmr-simulation.sh new file mode 100755 index 00000000000..0cf65ab5b8c --- /dev/null +++ b/apitest/scripts/trade-xmr-simulation.sh @@ -0,0 +1,124 @@ +#! /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 10), and bitcoin-core (v0.19, v0.20, or v0.21). +# +# - Bisq must be fully built with apitest dao setup files installed. +# Build command: `./gradlew clean build :apitest:installDaoSetup` +# +# - All supporting nodes must be run locally, in dev/dao/regtest mode: +# bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon +# +# These should be run using the apitest harness. From the root project dir, run: +# `$ ./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false` +# +# - Only regtest btc can be bought or sold with the test payment account. +# +# Usage: +# +# This script must be run from the root of the project, e.g.: +# +# `$ apitest/scripts/trade-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 getmyoffer --offer-id=$OFFER_ID" +printdate "ALICE CLI: $CMD" +OFFER=$($CMD) +exitoncommandalert $? +echo "$OFFER" +printbreak +sleeptraced 3 + +# Generate some btc blocks. +printdate "Generating btc blocks after publishing Alice's offer." +genbtcblocks 3 1 +printbreak + +# Go through the trade protocol. +executetrade +exitoncommandalert $? +printbreak + +# Get balances after trade completion. +printdate "Bob & Alice's balances after trade:" +printdate "ALICE CLI:" +printbalances "$ALICE_PORT" +printbreak +printdate "BOB CLI:" +printbalances "$BOB_PORT" +printbreak + +exit 0 diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index b17b154171a..97d818a30d6 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -57,6 +57,11 @@ public class ApiTestConfig { // Global constants public static final String BSQ = "BSQ"; public static final String BTC = "BTC"; + public static final String XMR = "XMR"; + public static final String EUR = "EUR"; + public static final String GBP = "GBP"; + public static final String RUBLE = "RUB"; + public static final String USD = "USD"; public static final String ARBITRATOR = "arbitrator"; public static final String MEDIATOR = "mediator"; public static final String REFUND_AGENT = "refundagent"; diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index c2701ffc441..9e731864a61 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -18,12 +18,17 @@ package bisq.apitest.method; import bisq.core.api.model.PaymentAccountForm; +import bisq.core.payment.CryptoCurrencyAccount; import bisq.core.payment.F2FAccount; +import bisq.core.payment.InstantCryptoCurrencyAccount; import bisq.core.payment.NationalBankAccount; +import bisq.core.payment.PaymentAccount; import bisq.core.proto.CoreProtoResolver; import bisq.common.util.Utilities; +import bisq.proto.grpc.BalancesInfo; + import java.io.File; import java.io.IOException; import java.io.PrintWriter; @@ -32,6 +37,8 @@ import java.util.stream.Collectors; import static bisq.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig; +import static bisq.cli.table.builder.TableType.BSQ_BALANCE_TBL; +import static bisq.cli.table.builder.TableType.BTC_BALANCE_TBL; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.stream; import static org.junit.jupiter.api.Assertions.fail; @@ -40,6 +47,7 @@ import bisq.apitest.ApiTestCase; import bisq.cli.GrpcClient; +import bisq.cli.table.builder.TableBuilder; public class MethodTest extends ApiTestCase { @@ -103,6 +111,11 @@ protected final File getPaymentAccountForm(GrpcClient grpcClient, String payment return jsonFile; } + protected final Function toCryptoCurrencyAccount = (proto) -> + (CryptoCurrencyAccount) PaymentAccount.fromProto(proto, CORE_PROTO_RESOLVER); + + protected final Function toInstantCryptoCurrencyAccount = (proto) -> + (InstantCryptoCurrencyAccount) PaymentAccount.fromProto(proto, CORE_PROTO_RESOLVER); protected bisq.core.payment.PaymentAccount createDummyF2FAccount(GrpcClient grpcClient, String countryCode) { @@ -119,7 +132,6 @@ protected bisq.core.payment.PaymentAccount createDummyF2FAccount(GrpcClient grpc return f2FAccount; } - protected bisq.core.payment.PaymentAccount createDummyBRLAccount(GrpcClient grpcClient, String holderName, String nationalAccountId, @@ -144,13 +156,21 @@ protected bisq.core.payment.PaymentAccount createDummyBRLAccount(GrpcClient grpc 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. + // subtype. 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 String formatBalancesTbls(BalancesInfo allBalances) { + StringBuilder balances = new StringBuilder("BTC"); + balances.append(new TableBuilder(BTC_BALANCE_TBL, allBalances.getBtc()).build()); + balances.append("BSQ"); + balances.append(new TableBuilder(BSQ_BALANCE_TBL, allBalances.getBsq()).build()); + return balances.toString(); + } + protected static String encodeToHex(String s) { return Utilities.bytesAsHexString(s.getBytes(UTF_8)); } 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 2d2e5fc6d73..b0eb42dc8a1 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java @@ -32,6 +32,7 @@ import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static bisq.apitest.config.ApiTestConfig.BSQ; +import static bisq.apitest.config.ApiTestConfig.XMR; import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static bisq.apitest.config.BisqAppConfig.arbdaemon; import static bisq.apitest.config.BisqAppConfig.bobdaemon; @@ -54,6 +55,8 @@ public abstract class AbstractOfferTest extends MethodTest { protected static PaymentAccount alicesBsqAcct; protected static PaymentAccount bobsBsqAcct; + protected static PaymentAccount alicesXmrAcct; + protected static PaymentAccount bobsXmrAcct; @BeforeAll public static void setUp() { @@ -84,27 +87,49 @@ public static void setUp() { return priceAsBigDecimal.multiply(factor).longValue(); }; - protected final BiFunction calcPriceAsLong = (base, delta) -> { + protected final BiFunction calcFiatTriggerPriceAsLong = (base, delta) -> { var priceAsDouble = new BigDecimal(base).add(new BigDecimal(delta)).doubleValue(); return Double.valueOf(exactMultiply(priceAsDouble, 10_000)).longValue(); }; + protected final BiFunction calcAltcoinTriggerPriceAsLong = (base, delta) -> { + var priceAsDouble = new BigDecimal(base).add(new BigDecimal(delta)).doubleValue(); + return Double.valueOf(exactMultiply(priceAsDouble, 100_000_000)).longValue(); + }; + protected final BiFunction calcPriceAsString = (base, delta) -> { var priceAsBigDecimal = new BigDecimal(Double.toString(base)) .add(new BigDecimal(Double.toString(delta))); return priceAsBigDecimal.toPlainString(); }; + @SuppressWarnings("ConstantConditions") public static void createBsqPaymentAccounts() { alicesBsqAcct = aliceClient.createCryptoCurrencyPaymentAccount("Alice's BSQ Account", BSQ, aliceClient.getUnusedBsqAddress(), false); + log.debug("Alices BSQ Account: {}", alicesBsqAcct); bobsBsqAcct = bobClient.createCryptoCurrencyPaymentAccount("Bob's BSQ Account", BSQ, bobClient.getUnusedBsqAddress(), false); + log.debug("Bob's BSQ Account: {}", bobsBsqAcct); + } + + @SuppressWarnings("ConstantConditions") + public static void createXmrPaymentAccounts() { + alicesXmrAcct = aliceClient.createCryptoCurrencyPaymentAccount("Alice's XMR Account", + XMR, + "44G4jWmSvTEfifSUZzTDnJVLPvYATmq9XhhtDqUof1BGCLceG82EQsVYG9Q9GN4bJcjbAJEc1JD1m5G7iK4UPZqACubV4Mq", + false); + log.debug("Alices XMR Account: {}", alicesXmrAcct); + bobsXmrAcct = bobClient.createCryptoCurrencyPaymentAccount("Bob's XMR Account", + XMR, + "4BDRhdSBKZqAXs3PuNTbMtaXBNqFj5idC2yMVnQj8Rm61AyKY8AxLTt9vGRJ8pwcG4EtpyD8YpGqdZWCZ2VZj6yVBN2RVKs", + false); + log.debug("Bob's XMR Account: {}", bobsXmrAcct); } @AfterAll diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateBSQOffersTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateBSQOffersTest.java index 652d7f50dcf..17de8305ec4 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateBSQOffersTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateBSQOffersTest.java @@ -32,10 +32,8 @@ import static bisq.apitest.config.ApiTestConfig.BSQ; import static bisq.apitest.config.ApiTestConfig.BTC; -import static bisq.cli.TableFormat.formatBalancesTbls; -import static bisq.cli.TableFormat.formatOfferTable; +import static bisq.cli.table.builder.TableType.OFFER_TBL; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; -import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -43,6 +41,10 @@ import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferPayload.Direction.SELL; + + +import bisq.cli.table.builder.TableBuilder; + @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @@ -70,7 +72,7 @@ public void testCreateBuy1BTCFor20KBSQOffer() { getDefaultBuyerSecurityDepositAsPercent(), alicesBsqAcct.getId(), MAKER_FEE_CURRENCY_CODE); - log.info("Sell BSQ (Buy BTC) OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); + log.info("Sell BSQ (Buy BTC) OFFER:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyPendingOffer()); @@ -117,7 +119,7 @@ public void testCreateSell1BTCFor20KBSQOffer() { getDefaultBuyerSecurityDepositAsPercent(), alicesBsqAcct.getId(), MAKER_FEE_CURRENCY_CODE); - log.info("SELL 20K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); + log.info("SELL 20K BSQ OFFER:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyPendingOffer()); @@ -164,7 +166,7 @@ public void testCreateBuyBTCWith1To2KBSQOffer() { getDefaultBuyerSecurityDepositAsPercent(), alicesBsqAcct.getId(), MAKER_FEE_CURRENCY_CODE); - log.info("BUY 1-2K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); + log.info("BUY 1-2K BSQ OFFER:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyPendingOffer()); @@ -211,7 +213,7 @@ public void testCreateSellBTCFor5To10KBSQOffer() { getDefaultBuyerSecurityDepositAsPercent(), alicesBsqAcct.getId(), MAKER_FEE_CURRENCY_CODE); - log.info("SELL 5-10K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); + log.info("SELL 5-10K BSQ OFFER:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyPendingOffer()); @@ -249,8 +251,8 @@ public void testCreateSellBTCFor5To10KBSQOffer() { @Test @Order(5) public void testGetAllMyBsqOffers() { - List offers = aliceClient.getMyBsqOffersSortedByDate(); - log.info("ALL ALICE'S BSQ OFFERS:\n{}", formatOfferTable(offers, BSQ)); + List offers = aliceClient.getMyCryptoCurrencyOffersSortedByDate(BSQ); + log.info("ALL ALICE'S BSQ OFFERS:\n{}", new TableBuilder(OFFER_TBL, offers).build()); assertEquals(4, offers.size()); log.info("ALICE'S BALANCES\n{}", formatBalancesTbls(aliceClient.getBalances())); } @@ -258,8 +260,8 @@ public void testGetAllMyBsqOffers() { @Test @Order(6) public void testGetAvailableBsqOffers() { - List offers = bobClient.getBsqOffersSortedByDate(); - log.info("ALL BOB'S AVAILABLE BSQ OFFERS:\n{}", formatOfferTable(offers, BSQ)); + List offers = bobClient.getCryptoCurrencyOffersSortedByDate(BSQ); + log.info("ALL BOB'S AVAILABLE BSQ OFFERS:\n{}", new TableBuilder(OFFER_TBL, offers).build()); assertEquals(4, offers.size()); log.info("BOB'S BALANCES\n{}", formatBalancesTbls(bobClient.getBalances())); } 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 715e05a92e7..1cf1017bc7b 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java @@ -29,9 +29,9 @@ import static bisq.apitest.config.ApiTestConfig.BSQ; import static bisq.apitest.config.ApiTestConfig.BTC; -import static bisq.cli.TableFormat.formatOfferTable; +import static bisq.apitest.config.ApiTestConfig.EUR; +import static bisq.cli.table.builder.TableType.OFFER_TBL; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; -import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -39,6 +39,10 @@ import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferPayload.Direction.SELL; + + +import bisq.cli.table.builder.TableBuilder; + @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @@ -58,7 +62,7 @@ public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() { getDefaultBuyerSecurityDepositAsPercent(), audAccount.getId(), MAKER_FEE_CURRENCY_CODE); - log.info("OFFER #1:\n{}", formatOfferTable(singletonList(newOffer), "AUD")); + log.info("OFFER #1:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyPendingOffer()); @@ -103,7 +107,7 @@ public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() { getDefaultBuyerSecurityDepositAsPercent(), usdAccount.getId(), MAKER_FEE_CURRENCY_CODE); - log.info("OFFER #2:\n{}", formatOfferTable(singletonList(newOffer), "USD")); + log.info("OFFER #2:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyPendingOffer()); @@ -148,7 +152,7 @@ public void testCreateEURBTCSellOfferUsingFixedPrice95001234() { getDefaultBuyerSecurityDepositAsPercent(), eurAccount.getId(), MAKER_FEE_CURRENCY_CODE); - log.info("OFFER #3:\n{}", formatOfferTable(singletonList(newOffer), "EUR")); + log.info("OFFER #3:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyPendingOffer()); @@ -162,7 +166,7 @@ public void testCreateEURBTCSellOfferUsingFixedPrice95001234() { assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(eurAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(BTC, newOffer.getBaseCurrencyCode()); - assertEquals("EUR", newOffer.getCounterCurrencyCode()); + assertEquals(EUR, newOffer.getCounterCurrencyCode()); assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = aliceClient.getMyOffer(newOfferId); @@ -177,7 +181,7 @@ public void testCreateEURBTCSellOfferUsingFixedPrice95001234() { assertEquals(1_500_000, newOffer.getBuyerSecurityDeposit()); assertEquals(eurAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(BTC, newOffer.getBaseCurrencyCode()); - assertEquals("EUR", newOffer.getCounterCurrencyCode()); + assertEquals(EUR, newOffer.getCounterCurrencyCode()); assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); } } 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 391bb4c5a37..50bbd8ceccb 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -38,7 +38,7 @@ import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.config.ApiTestConfig.BTC; -import static bisq.cli.TableFormat.formatOfferTable; +import static bisq.cli.table.builder.TableType.OFFER_TBL; import static bisq.common.util.MathUtils.roundDouble; import static bisq.common.util.MathUtils.scaleDownByPowerOf10; import static bisq.common.util.MathUtils.scaleUpByPowerOf10; @@ -47,7 +47,6 @@ import static java.lang.Math.abs; import static java.lang.String.format; import static java.math.RoundingMode.HALF_UP; -import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -55,6 +54,10 @@ import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferPayload.Direction.SELL; + + +import bisq.cli.table.builder.TableBuilder; + @SuppressWarnings("ConstantConditions") @Disabled @Slf4j @@ -81,7 +84,7 @@ public void testCreateUSDBTCBuyOffer5PctPriceMargin() { usdAccount.getId(), MAKER_FEE_CURRENCY_CODE, NO_TRIGGER_PRICE); - log.info("OFFER #1:\n{}", formatOfferTable(singletonList(newOffer), "usd")); + log.info("OFFER #1:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyPendingOffer()); @@ -128,7 +131,7 @@ public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() { nzdAccount.getId(), MAKER_FEE_CURRENCY_CODE, NO_TRIGGER_PRICE); - log.info("OFFER #2:\n{}", formatOfferTable(singletonList(newOffer), "nzd")); + log.info("OFFER #2:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyPendingOffer()); @@ -175,7 +178,7 @@ public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() { gbpAccount.getId(), MAKER_FEE_CURRENCY_CODE, NO_TRIGGER_PRICE); - log.info("OFFER #3:\n{}", formatOfferTable(singletonList(newOffer), "gbp")); + log.info("OFFER #3:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyPendingOffer()); @@ -222,7 +225,7 @@ public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() { brlAccount.getId(), MAKER_FEE_CURRENCY_CODE, NO_TRIGGER_PRICE); - log.info("OFFER #4:\n{}", formatOfferTable(singletonList(newOffer), "brl")); + log.info("OFFER #4:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyPendingOffer()); @@ -278,7 +281,7 @@ public void testCreateUSDBTCBuyOfferWithTriggerPrice() { genBtcBlocksThenWait(1, 4000); // give time to add to offer book newOffer = aliceClient.getMyOffer(newOffer.getId()); - log.info("OFFER #5:\n{}", formatOfferTable(singletonList(newOffer), "usd")); + log.info("OFFER #5:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); assertTrue(newOffer.getIsMyOffer()); assertFalse(newOffer.getIsMyPendingOffer()); assertEquals(triggerPriceAsLong, newOffer.getTriggerPrice()); 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 00000000000..898e67cb263 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateXMROffersTest.java @@ -0,0 +1,287 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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.BSQ; +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.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +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 bisq.cli.table.builder.TableBuilder; + +@SuppressWarnings("ConstantConditions") +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class CreateXMROffersTest extends AbstractOfferTest { + + private static final String MAKER_FEE_CURRENCY_CODE = BSQ; + + @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 BSQ, XMR, etc. In this test case, + // Alice places an offer to BUY BTC with BSQ. + var newOffer = aliceClient.createFixedPricedOffer(BUY.name(), + XMR, + 100_000_000L, + 75_000_000L, + "0.005", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR + getDefaultBuyerSecurityDepositAsPercent(), + alicesXmrAcct.getId(), + MAKER_FEE_CURRENCY_CODE); + log.info("Sell XMR (Buy BTC) OFFER:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + + String newOfferId = newOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(BUY.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(500_000L, 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()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + + genBtcBlockAndWaitForOfferPreparation(); + + newOffer = aliceClient.getMyOffer(newOfferId); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); + assertEquals(newOfferId, newOffer.getId()); + assertEquals(BUY.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(500_000L, 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()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + } + + @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 + getDefaultBuyerSecurityDepositAsPercent(), + alicesXmrAcct.getId(), + MAKER_FEE_CURRENCY_CODE); + log.info("Buy XMR (Sell BTC) OFFER:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + + String newOfferId = newOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(SELL.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(500_000L, 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()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + + genBtcBlockAndWaitForOfferPreparation(); + + newOffer = aliceClient.getMyOffer(newOfferId); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); + assertEquals(newOfferId, newOffer.getId()); + assertEquals(SELL.name(), newOffer.getDirection()); + assertFalse(newOffer.getUseMarketBasedPrice()); + assertEquals(500_000L, 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()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + } + + @Test + @Order(3) + public void testCreatePriceMarginBasedBuy1BTCOfferWithTriggerPrice() { + double priceMarginPctInput = 1.00; + double mktPriceAsDouble = aliceClient.getBtcPrice(XMR); + long triggerPriceAsLong = calcAltcoinTriggerPriceAsLong.apply(mktPriceAsDouble, -0.001); + var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), + XMR, + 100_000_000L, + 75_000_000L, + priceMarginPctInput, + getDefaultBuyerSecurityDepositAsPercent(), + alicesXmrAcct.getId(), + MAKER_FEE_CURRENCY_CODE, + triggerPriceAsLong); + log.info("PENDING Sell XMR (Buy BTC) OFFER:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + + String newOfferId = newOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(BUY.name(), newOffer.getDirection()); + assertTrue(newOffer.getUseMarketBasedPrice()); + + // There is no trigger price while offer is pending. + assertEquals(0, 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()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + + genBtcBlockAndWaitForOfferPreparation(); + + newOffer = aliceClient.getMyOffer(newOfferId); + log.info("AVAILABLE Sell XMR (Buy BTC) OFFER:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); + assertEquals(newOfferId, newOffer.getId()); + assertEquals(BUY.name(), newOffer.getDirection()); + assertTrue(newOffer.getUseMarketBasedPrice()); + + // The trigger price should exist on the prepared offer. + assertEquals(triggerPriceAsLong, 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()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + } + + @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, + getDefaultBuyerSecurityDepositAsPercent(), + alicesXmrAcct.getId(), + MAKER_FEE_CURRENCY_CODE, + NO_TRIGGER_PRICE); + log.info("Buy XMR (Sell BTC) OFFER:\n{}", new TableBuilder(OFFER_TBL, newOffer).build()); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + + 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()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + + genBtcBlockAndWaitForOfferPreparation(); + + newOffer = aliceClient.getMyOffer(newOfferId); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); + 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()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); + } + + @Test + @Order(5) + public void testGetAllMyXMROffers() { + List offers = aliceClient.getMyCryptoCurrencyOffersSortedByDate(XMR); + log.info("ALL ALICE'S XMR OFFERS:\n{}", new TableBuilder(OFFER_TBL, offers).build()); + assertEquals(4, offers.size()); + log.info("ALICE'S BALANCES\n{}", formatBalancesTbls(aliceClient.getBalances())); + } + + @Test + @Order(6) + public void testGetAvailableXMROffers() { + List offers = bobClient.getCryptoCurrencyOffersSortedByDate(XMR); + log.info("ALL BOB'S AVAILABLE XMR OFFERS:\n{}", new TableBuilder(OFFER_TBL, offers).build()); + assertEquals(4, offers.size()); + log.info("BOB'S BALANCES\n{}", formatBalancesTbls(bobClient.getBalances())); + } + + private void genBtcBlockAndWaitForOfferPreparation() { + // Extra time is needed for the OfferUtils#isBsqForMakerFeeAvailable, which + // can sometimes return an incorrect false value if the BsqWallet's + // available confirmed balance is temporarily = zero during BSQ offer prep. + genBtcBlocksThenWait(1, 5000); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java index a947044d07c..b343b0cdb54 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java @@ -37,12 +37,11 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import static bisq.apitest.config.ApiTestConfig.BSQ; -import static bisq.cli.TableFormat.formatOfferTable; +import static bisq.apitest.config.ApiTestConfig.*; +import static bisq.cli.table.builder.TableType.OFFER_TBL; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; import static bisq.proto.grpc.EditOfferRequest.EditType.*; import static java.lang.String.format; -import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -50,6 +49,10 @@ import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferPayload.Direction.SELL; + + +import bisq.cli.table.builder.TableBuilder; + @SuppressWarnings("ALL") @Disabled @Slf4j @@ -57,21 +60,19 @@ public class EditOfferTest extends AbstractOfferTest { // Some test fixtures to reduce duplication. - private static final Map paymentAcctCache = new HashMap<>(); - private static final String DOLLAR = "USD"; - private static final String RUBLE = "RUB"; + private static final Map fiatPaymentAcctCache = new HashMap<>(); private static final long AMOUNT = 10000000L; @Test @Order(1) public void testOfferDisableAndEnable() { - PaymentAccount paymentAcct = getOrCreatePaymentAccount("DE"); + PaymentAccount paymentAcct = getOrCreateFiatPaymentAccount("DE"); OfferInfo originalOffer = createMktPricedOfferForEdit(BUY.name(), - "EUR", + EUR, paymentAcct.getId(), 0.0, NO_TRIGGER_PRICE); - log.info("ORIGINAL EUR OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "EUR")); + log.info("ORIGINAL EUR OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); assertFalse(originalOffer.getIsActivated()); // Not activated until prep is done. genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. originalOffer = aliceClient.getMyOffer(originalOffer.getId()); @@ -80,13 +81,13 @@ public void testOfferDisableAndEnable() { aliceClient.editOfferActivationState(originalOffer.getId(), DEACTIVATE_OFFER); genBtcBlocksThenWait(1, 1500); // Wait for offer book removal. OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - log.info("EDITED EUR OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "EUR")); + log.info("EDITED EUR OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); assertFalse(editedOffer.getIsActivated()); // Re-enable offer aliceClient.editOfferActivationState(editedOffer.getId(), ACTIVATE_OFFER); genBtcBlocksThenWait(1, 1500); // Wait for offer book re-entry. editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - log.info("EDITED EUR OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "EUR")); + log.info("EDITED EUR OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); assertTrue(editedOffer.getIsActivated()); doSanityCheck(originalOffer, editedOffer); @@ -95,26 +96,26 @@ public void testOfferDisableAndEnable() { @Test @Order(2) public void testEditTriggerPrice() { - PaymentAccount paymentAcct = getOrCreatePaymentAccount("FI"); + PaymentAccount paymentAcct = getOrCreateFiatPaymentAccount("FI"); OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), - "EUR", + EUR, paymentAcct.getId(), 0.0, NO_TRIGGER_PRICE); - log.info("ORIGINAL EUR OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "EUR")); + log.info("ORIGINAL EUR OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. originalOffer = aliceClient.getMyOffer(originalOffer.getId()); assertEquals(0 /*no trigger price*/, originalOffer.getTriggerPrice()); // Edit the offer's trigger price, nothing else. - var mktPrice = aliceClient.getBtcPrice("EUR"); + var mktPrice = aliceClient.getBtcPrice(EUR); var delta = 5_000.00; - var newTriggerPriceAsLong = calcPriceAsLong.apply(mktPrice, delta); + var newTriggerPriceAsLong = calcFiatTriggerPriceAsLong.apply(mktPrice, delta); aliceClient.editOfferTriggerPrice(originalOffer.getId(), newTriggerPriceAsLong); sleep(2500); // Wait for offer book re-entry. OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - log.info("EDITED EUR OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "EUR")); + log.info("EDITED EUR OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); assertEquals(newTriggerPriceAsLong, editedOffer.getTriggerPrice()); assertTrue(editedOffer.getUseMarketBasedPrice()); @@ -124,13 +125,13 @@ public void testEditTriggerPrice() { @Test @Order(3) public void testSetTriggerPriceToNegativeValueShouldThrowException() { - PaymentAccount paymentAcct = getOrCreatePaymentAccount("FI"); + PaymentAccount paymentAcct = getOrCreateFiatPaymentAccount("FI"); final OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), - "EUR", + EUR, paymentAcct.getId(), 0.0, NO_TRIGGER_PRICE); - log.info("ORIGINAL EUR OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "EUR")); + log.info("ORIGINAL EUR OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. // Edit the offer's trigger price, set to -1, check error. Throwable exception = assertThrows(StatusRuntimeException.class, () -> @@ -144,21 +145,21 @@ public void testSetTriggerPriceToNegativeValueShouldThrowException() { @Test @Order(4) public void testEditMktPriceMargin() { - PaymentAccount paymentAcct = getOrCreatePaymentAccount("US"); + PaymentAccount paymentAcct = getOrCreateFiatPaymentAccount("US"); var originalMktPriceMargin = new BigDecimal("0.1").doubleValue(); OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), - DOLLAR, + USD, paymentAcct.getId(), originalMktPriceMargin, NO_TRIGGER_PRICE); - log.info("ORIGINAL USD OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "USD")); + log.info("ORIGINAL USD OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. assertEquals(scaledDownMktPriceMargin.apply(originalMktPriceMargin), originalOffer.getMarketPriceMargin()); // Edit the offer's price margin, nothing else. var newMktPriceMargin = new BigDecimal("0.5").doubleValue(); aliceClient.editOfferPriceMargin(originalOffer.getId(), newMktPriceMargin); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - log.info("EDITED USD OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "USD")); + log.info("EDITED USD OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); doSanityCheck(originalOffer, editedOffer); @@ -167,14 +168,14 @@ public void testEditMktPriceMargin() { @Test @Order(5) public void testEditFixedPrice() { - PaymentAccount paymentAcct = getOrCreatePaymentAccount("RU"); + PaymentAccount paymentAcct = getOrCreateFiatPaymentAccount("RU"); double mktPriceAsDouble = aliceClient.getBtcPrice(RUBLE); String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 200_000.0000); OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(), RUBLE, paymentAcct.getId(), fixedPriceAsString); - log.info("ORIGINAL RUB OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "RUB")); + log.info("ORIGINAL RUB OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. // Edit the offer's fixed price, nothing else. String editedFixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 100_000.0000); @@ -182,7 +183,7 @@ public void testEditFixedPrice() { // Wait for edited offer to be removed from offer-book, edited, and re-published. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - log.info("EDITED RUB OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "RUB")); + log.info("EDITED RUB OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); var expectedNewFixedPrice = scaledUpFiatOfferPrice.apply(new BigDecimal(editedFixedPriceAsString)); assertEquals(expectedNewFixedPrice, editedOffer.getPrice()); assertFalse(editedOffer.getUseMarketBasedPrice()); @@ -193,14 +194,14 @@ public void testEditFixedPrice() { @Test @Order(6) public void testEditFixedPriceAndDeactivation() { - PaymentAccount paymentAcct = getOrCreatePaymentAccount("RU"); + PaymentAccount paymentAcct = getOrCreateFiatPaymentAccount("RU"); double mktPriceAsDouble = aliceClient.getBtcPrice(RUBLE); String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 200_000.0000); OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(), RUBLE, paymentAcct.getId(), fixedPriceAsString); - log.info("ORIGINAL RUB OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "RUB")); + log.info("ORIGINAL RUB OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. // Edit the offer's fixed price and deactivate it. String editedFixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 100_000.0000); @@ -214,7 +215,7 @@ public void testEditFixedPriceAndDeactivation() { // Wait for edited offer to be removed from offer-book, edited, and re-published. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - log.info("EDITED RUB OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "RUB")); + log.info("EDITED RUB OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); var expectedNewFixedPrice = scaledUpFiatOfferPrice.apply(new BigDecimal(editedFixedPriceAsString)); assertEquals(expectedNewFixedPrice, editedOffer.getPrice()); assertFalse(editedOffer.getIsActivated()); @@ -225,15 +226,15 @@ public void testEditFixedPriceAndDeactivation() { @Test @Order(7) public void testEditMktPriceMarginAndDeactivation() { - PaymentAccount paymentAcct = getOrCreatePaymentAccount("US"); + PaymentAccount paymentAcct = getOrCreateFiatPaymentAccount("US"); var originalMktPriceMargin = new BigDecimal("0.0").doubleValue(); OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), - DOLLAR, + USD, paymentAcct.getId(), originalMktPriceMargin, 0); - log.info("ORIGINAL USD OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "USD")); + log.info("ORIGINAL USD OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. originalOffer = aliceClient.getMyOffer(originalOffer.getId()); assertEquals(scaledDownMktPriceMargin.apply(originalMktPriceMargin), originalOffer.getMarketPriceMargin()); @@ -250,7 +251,7 @@ public void testEditMktPriceMarginAndDeactivation() { // Wait for edited offer to be removed from offer-book, edited, and re-published. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - log.info("EDITED USD OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "USD")); + log.info("EDITED USD OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); assertEquals(0, editedOffer.getTriggerPrice()); assertFalse(editedOffer.getIsActivated()); @@ -261,26 +262,26 @@ public void testEditMktPriceMarginAndDeactivation() { @Test @Order(8) public void testEditMktPriceMarginAndTriggerPriceAndDeactivation() { - PaymentAccount paymentAcct = getOrCreatePaymentAccount("US"); + PaymentAccount paymentAcct = getOrCreateFiatPaymentAccount("US"); var originalMktPriceMargin = new BigDecimal("0.0").doubleValue(); - var mktPriceAsDouble = aliceClient.getBtcPrice(DOLLAR); - var originalTriggerPriceAsLong = calcPriceAsLong.apply(mktPriceAsDouble, -5_000.0000); - + var mktPriceAsDouble = aliceClient.getBtcPrice(USD); + var originalTriggerPriceAsLong = calcFiatTriggerPriceAsLong.apply(mktPriceAsDouble, -5_000.0000); OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), - DOLLAR, + USD, paymentAcct.getId(), originalMktPriceMargin, originalTriggerPriceAsLong); - log.info("ORIGINAL USD OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "USD")); + log.info("PENDING USD OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. originalOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("ORIGINAL USD OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); assertEquals(scaledDownMktPriceMargin.apply(originalMktPriceMargin), originalOffer.getMarketPriceMargin()); assertEquals(originalTriggerPriceAsLong, originalOffer.getTriggerPrice()); // Edit the offer's price margin and trigger price, and deactivate it. var newMktPriceMargin = new BigDecimal("0.1").doubleValue(); - var newTriggerPriceAsLong = calcPriceAsLong.apply(mktPriceAsDouble, -2_000.0000); + var newTriggerPriceAsLong = calcFiatTriggerPriceAsLong.apply(mktPriceAsDouble, -2_000.0000); aliceClient.editOffer(originalOffer.getId(), "0.00", originalOffer.getUseMarketBasedPrice(), @@ -291,7 +292,7 @@ public void testEditMktPriceMarginAndTriggerPriceAndDeactivation() { // Wait for edited offer to be removed from offer-book, edited, and re-published. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - log.info("EDITED USD OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "USD")); + log.info("EDITED USD OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); assertEquals(newTriggerPriceAsLong, editedOffer.getTriggerPrice()); assertFalse(editedOffer.getIsActivated()); @@ -302,14 +303,14 @@ public void testEditMktPriceMarginAndTriggerPriceAndDeactivation() { @Test @Order(9) public void testEditingFixedPriceInMktPriceMarginBasedOfferShouldThrowException() { - PaymentAccount paymentAcct = getOrCreatePaymentAccount("US"); + PaymentAccount paymentAcct = getOrCreateFiatPaymentAccount("US"); var originalMktPriceMargin = new BigDecimal("0.0").doubleValue(); final OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), - DOLLAR, + USD, paymentAcct.getId(), originalMktPriceMargin, NO_TRIGGER_PRICE); - log.info("ORIGINAL USD OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "USD")); + log.info("ORIGINAL USD OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. // Try to edit both the fixed price and mkt price margin. var newMktPriceMargin = new BigDecimal("0.25").doubleValue(); @@ -333,22 +334,21 @@ public void testEditingFixedPriceInMktPriceMarginBasedOfferShouldThrowException( @Test @Order(10) public void testEditingTriggerPriceInFixedPriceOfferShouldThrowException() { - PaymentAccount paymentAcct = getOrCreatePaymentAccount("RU"); + PaymentAccount paymentAcct = getOrCreateFiatPaymentAccount("RU"); double mktPriceAsDouble = aliceClient.getBtcPrice(RUBLE); String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 200_000.0000); OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(), RUBLE, paymentAcct.getId(), fixedPriceAsString); - log.info("ORIGINAL RUB OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "RUB")); + log.info("ORIGINAL RUB OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. - long newTriggerPrice = 1000000L; + long newTriggerPrice = 1_000_000L; Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.editOfferTriggerPrice(originalOffer.getId(), newTriggerPrice)); String expectedExceptionMessage = - format("UNKNOWN: programmer error: cannot set a trigger price (%s) in" + format("UNKNOWN: programmer error: cannot set a trigger price in" + " fixed price offer with id '%s'", - newTriggerPrice, originalOffer.getId()); assertEquals(expectedExceptionMessage, exception.getMessage()); } @@ -356,20 +356,20 @@ public void testEditingTriggerPriceInFixedPriceOfferShouldThrowException() { @Test @Order(11) public void testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice() { - PaymentAccount paymentAcct = getOrCreatePaymentAccount("MX"); + PaymentAccount paymentAcct = getOrCreateFiatPaymentAccount("MX"); double mktPriceAsDouble = aliceClient.getBtcPrice("MXN"); String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 0.00); OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(), "MXN", paymentAcct.getId(), fixedPriceAsString); - log.info("ORIGINAL MXN OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "MXN")); + log.info("PENDING MXN OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. // Change the offer to mkt price based and set a trigger price. var newMktPriceMargin = new BigDecimal("0.05").doubleValue(); var delta = 200_000.0000; // trigger price on buy offer is 200K above mkt price - var newTriggerPriceAsLong = calcPriceAsLong.apply(mktPriceAsDouble, delta); + var newTriggerPriceAsLong = calcFiatTriggerPriceAsLong.apply(mktPriceAsDouble, delta); aliceClient.editOffer(originalOffer.getId(), "0.00", true, @@ -380,7 +380,7 @@ public void testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice() { // Wait for edited offer to be removed from offer-book, edited, and re-published. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - log.info("EDITED MXN OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "MXN")); + log.info("EDITED MXN OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); assertTrue(editedOffer.getUseMarketBasedPrice()); assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); assertEquals(newTriggerPriceAsLong, editedOffer.getTriggerPrice()); @@ -392,19 +392,22 @@ public void testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice() { @Test @Order(12) public void testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt() { - PaymentAccount paymentAcct = getOrCreatePaymentAccount("GB"); - double mktPriceAsDouble = aliceClient.getBtcPrice("GBP"); + PaymentAccount paymentAcct = getOrCreateFiatPaymentAccount("GB"); + double mktPriceAsDouble = aliceClient.getBtcPrice(GBP); var originalMktPriceMargin = new BigDecimal("0.25").doubleValue(); var delta = 1_000.0000; // trigger price on sell offer is 1K below mkt price - var originalTriggerPriceAsLong = calcPriceAsLong.apply(mktPriceAsDouble, delta); - final OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), - "GBP", + var originalTriggerPriceAsLong = calcFiatTriggerPriceAsLong.apply(mktPriceAsDouble, delta); + OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), + GBP, paymentAcct.getId(), originalMktPriceMargin, originalTriggerPriceAsLong); - log.info("ORIGINAL GBP OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "GBP")); + log.info("PENDING GBP OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + originalOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("ORIGINAL GBP OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); + String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 0.00); aliceClient.editOffer(originalOffer.getId(), fixedPriceAsString, @@ -416,14 +419,16 @@ public void testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt() { // Wait for edited offer to be removed from offer-book, edited, and re-published. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - log.info("EDITED GBP OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "GBP")); + log.info("EDITED GBP OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); assertEquals(scaledUpFiatOfferPrice.apply(new BigDecimal(fixedPriceAsString)), editedOffer.getPrice()); - assertFalse(editedOffer.getUseMarketBasedPrice()); - assertEquals(0.00, editedOffer.getMarketPriceMargin()); - assertEquals(0, editedOffer.getTriggerPrice()); assertFalse(editedOffer.getIsActivated()); + assertMarketBasedPriceFieldsAreIgnored(editedOffer); + + doSanityCheck(originalOffer, editedOffer); } + // Edit BSQ Offers + @Test @Order(13) public void testChangeFixedPricedBsqOfferToPriceMarginBasedOfferShouldThrowException() { @@ -436,7 +441,7 @@ public void testChangeFixedPricedBsqOfferToPriceMarginBasedOfferShouldThrowExcep getDefaultBuyerSecurityDepositAsPercent(), alicesBsqAcct.getId(), BSQ); - log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ")); + log.info("ORIGINAL BSQ OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.editOffer(originalOffer.getId(), @@ -447,7 +452,7 @@ public void testChangeFixedPricedBsqOfferToPriceMarginBasedOfferShouldThrowExcep ACTIVATE_OFFER, MKT_PRICE_MARGIN_ONLY)); String expectedExceptionMessage = format("UNKNOWN: cannot set mkt price margin or" - + " trigger price on fixed price altcoin offer with id '%s'", + + " trigger price on fixed price bsq offer with id '%s'", originalOffer.getId()); assertEquals(expectedExceptionMessage, exception.getMessage()); } @@ -464,9 +469,9 @@ public void testEditTriggerPriceOnFixedPriceBsqOfferShouldThrowException() { getDefaultBuyerSecurityDepositAsPercent(), alicesBsqAcct.getId(), BSQ); - log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ")); + log.info("ORIGINAL BSQ OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. - var newTriggerPriceAsLong = calcPriceAsLong.apply(0.00005, 0.00); + var newTriggerPriceAsLong = calcAltcoinTriggerPriceAsLong.apply(0.00005, 0.000055); Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.editOffer(originalOffer.getId(), "0.00", @@ -476,7 +481,7 @@ public void testEditTriggerPriceOnFixedPriceBsqOfferShouldThrowException() { ACTIVATE_OFFER, TRIGGER_PRICE_ONLY)); String expectedExceptionMessage = format("UNKNOWN: cannot set mkt price margin or" - + " trigger price on fixed price altcoin offer with id '%s'", + + " trigger price on fixed price bsq offer with id '%s'", originalOffer.getId()); assertEquals(expectedExceptionMessage, exception.getMessage()); } @@ -494,7 +499,7 @@ public void testEditFixedPriceOnBsqOffer() { getDefaultBuyerSecurityDepositAsPercent(), alicesBsqAcct.getId(), BSQ); - log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ")); + log.info("ORIGINAL BSQ OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. String newFixedPriceAsString = "0.00003111"; aliceClient.editOffer(originalOffer.getId(), @@ -507,12 +512,12 @@ public void testEditFixedPriceOnBsqOffer() { // Wait for edited offer to be edited and removed from offer-book. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - log.info("EDITED BSQ OFFER:\n{}", formatOfferTable(singletonList(editedOffer), BSQ)); + log.info("EDITED BSQ OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); assertEquals(scaledUpAltcoinOfferPrice.apply(newFixedPriceAsString), editedOffer.getPrice()); assertTrue(editedOffer.getIsActivated()); - assertFalse(editedOffer.getUseMarketBasedPrice()); - assertEquals(0.00, editedOffer.getMarketPriceMargin()); - assertEquals(0, editedOffer.getTriggerPrice()); + assertMarketBasedPriceFieldsAreIgnored(editedOffer); + + doSanityCheck(originalOffer, editedOffer); } @Test @@ -528,7 +533,7 @@ public void testDisableBsqOffer() { getDefaultBuyerSecurityDepositAsPercent(), alicesBsqAcct.getId(), BSQ); - log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ")); + log.info("ORIGINAL BSQ OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. aliceClient.editOffer(originalOffer.getId(), fixedPriceAsString, @@ -540,12 +545,12 @@ public void testDisableBsqOffer() { // Wait for edited offer to be removed from offer-book. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - log.info("EDITED BSQ OFFER:\n{}", formatOfferTable(singletonList(editedOffer), BSQ)); + log.info("EDITED BSQ OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); assertFalse(editedOffer.getIsActivated()); assertEquals(scaledUpAltcoinOfferPrice.apply(fixedPriceAsString), editedOffer.getPrice()); - assertFalse(editedOffer.getUseMarketBasedPrice()); - assertEquals(0.00, editedOffer.getMarketPriceMargin()); - assertEquals(0, editedOffer.getTriggerPrice()); + assertMarketBasedPriceFieldsAreIgnored(editedOffer); + + doSanityCheck(originalOffer, editedOffer); } @Test @@ -561,7 +566,7 @@ public void testEditFixedPriceAndDisableBsqOffer() { getDefaultBuyerSecurityDepositAsPercent(), alicesBsqAcct.getId(), BSQ); - log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ")); + log.info("ORIGINAL BSQ OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. String newFixedPriceAsString = "0.000045"; aliceClient.editOffer(originalOffer.getId(), @@ -574,12 +579,227 @@ public void testEditFixedPriceAndDisableBsqOffer() { // Wait for edited offer to be edited and removed from offer-book. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - log.info("EDITED BSQ OFFER:\n{}", formatOfferTable(singletonList(editedOffer), BSQ)); + log.info("EDITED BSQ OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); assertFalse(editedOffer.getIsActivated()); assertEquals(scaledUpAltcoinOfferPrice.apply(newFixedPriceAsString), editedOffer.getPrice()); + assertMarketBasedPriceFieldsAreIgnored(editedOffer); + + doSanityCheck(originalOffer, editedOffer); + } + + // Edit XMR Offers + + @Test + @Order(18) + public void testChangePriceMarginBasedXmrOfferWithTriggerPriceToFixedPricedAndDeactivateIt() { + createXmrPaymentAccounts(); + double mktPriceAsDouble = aliceClient.getBtcPrice(XMR); + long triggerPriceAsLong = calcAltcoinTriggerPriceAsLong.apply(mktPriceAsDouble, 0.001); + OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), + XMR, + alicesXmrAcct.getId(), + 0.0, + triggerPriceAsLong); + log.info("PENDING XMR OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + + originalOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("ORIGINAL XMR OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); + + String newFixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, -0.001); + aliceClient.editOffer(originalOffer.getId(), + newFixedPriceAsString, + false, + 0.00, + 0, + DEACTIVATE_OFFER, + FIXED_PRICE_AND_ACTIVATION_STATE); + // Wait for edited offer to be removed from offer-book, edited & not re-published. + genBtcBlocksThenWait(1, 2500); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED XMR OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); + assertEquals(scaledUpAltcoinOfferPrice.apply(newFixedPriceAsString), editedOffer.getPrice()); assertFalse(editedOffer.getUseMarketBasedPrice()); assertEquals(0.00, editedOffer.getMarketPriceMargin()); assertEquals(0, editedOffer.getTriggerPrice()); + assertFalse(editedOffer.getIsActivated()); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(19) + public void testChangeFixedPricedXmrOfferToPriceMarginBasedOfferWithTriggerPrice() { + createXmrPaymentAccounts(); + double mktPriceAsDouble = aliceClient.getBtcPrice(XMR); + String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 0.00); + OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(), + XMR, + 100_000_000L, + 50_000_000L, + fixedPriceAsString, // FIXED PRICE IN BTC (satoshis) FOR 1 XMR + getDefaultBuyerSecurityDepositAsPercent(), + alicesXmrAcct.getId(), + BSQ); + log.info("PENDING XMR OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + + originalOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("ORIGINAL XMR OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); + + // Change the offer to mkt price based and set a trigger price. + var newMktPriceMargin = new BigDecimal("0.05").doubleValue(); + var delta = -0.00100000; + var newTriggerPriceAsLong = calcAltcoinTriggerPriceAsLong.apply(mktPriceAsDouble, delta); + aliceClient.editOffer(originalOffer.getId(), + "0.00", + true, + newMktPriceMargin, + newTriggerPriceAsLong, + ACTIVATE_OFFER, + MKT_PRICE_MARGIN_AND_TRIGGER_PRICE); + // Wait for edited offer to be removed from offer-book, edited, and re-published. + genBtcBlocksThenWait(1, 2500); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED XMR OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); + assertTrue(editedOffer.getUseMarketBasedPrice()); + assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); + assertEquals(newTriggerPriceAsLong, editedOffer.getTriggerPrice()); + assertTrue(editedOffer.getIsActivated()); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(20) + public void testEditTriggerPriceOnFixedPriceXmrOfferShouldThrowException() { + createXmrPaymentAccounts(); + OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(), + XMR, + 100_000_000L, + 25_000_000L, + "0.007", // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ + getDefaultBuyerSecurityDepositAsPercent(), + alicesXmrAcct.getId(), + BSQ); + log.info("ORIGINAL XMR OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + + var newTriggerPriceAsLong = calcAltcoinTriggerPriceAsLong.apply(0.007, 0.001); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + aliceClient.editOffer(originalOffer.getId(), + "0.00", + false, + 0.1, + newTriggerPriceAsLong, + ACTIVATE_OFFER, + TRIGGER_PRICE_ONLY)); + String expectedExceptionMessage = format("UNKNOWN: programmer error: cannot set a trigger price" + + " in fixed price offer with id '%s'", + originalOffer.getId()); + assertEquals(expectedExceptionMessage, exception.getMessage()); + } + + + @Test + @Order(21) + public void testEditFixedPriceOnXmrOffer() { + createXmrPaymentAccounts(); + String fixedPriceAsString = "0.008"; // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ + final OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(), + XMR, + 100_000_000L, + 100_000_000L, + fixedPriceAsString, + getDefaultBuyerSecurityDepositAsPercent(), + alicesXmrAcct.getId(), + BSQ); + log.info("ORIGINAL BSQ OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + String newFixedPriceAsString = "0.009"; + aliceClient.editOffer(originalOffer.getId(), + newFixedPriceAsString, + false, + 0.0, + 0, + ACTIVATE_OFFER, + FIXED_PRICE_ONLY); + // Wait for edited offer to be edited and removed from offer-book. + genBtcBlocksThenWait(1, 2500); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED XMR OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); + assertEquals(scaledUpAltcoinOfferPrice.apply(newFixedPriceAsString), editedOffer.getPrice()); + assertTrue(editedOffer.getIsActivated()); + assertMarketBasedPriceFieldsAreIgnored(editedOffer); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(22) + public void testDisableXmrOffer() { + createXmrPaymentAccounts(); + String fixedPriceAsString = "0.008"; // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ + final OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(), + XMR, + 100_000_000L, + 50_000_000L, + fixedPriceAsString, + getDefaultBuyerSecurityDepositAsPercent(), + alicesXmrAcct.getId(), + BSQ); + log.info("ORIGINAL XMR OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + aliceClient.editOffer(originalOffer.getId(), + fixedPriceAsString, + false, + 0.0, + 0, + DEACTIVATE_OFFER, + ACTIVATION_STATE_ONLY); + // Wait for edited offer to be removed from offer-book. + genBtcBlocksThenWait(1, 2500); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED XMR OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); + assertFalse(editedOffer.getIsActivated()); + assertEquals(scaledUpAltcoinOfferPrice.apply(fixedPriceAsString), editedOffer.getPrice()); + assertMarketBasedPriceFieldsAreIgnored(editedOffer); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(23) + public void testEditFixedPriceAndDisableXmrOffer() { + createXmrPaymentAccounts(); + String fixedPriceAsString = "0.004"; // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ + final OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(), + XMR, + 100_000_000L, + 100_000_000L, + fixedPriceAsString, + getDefaultBuyerSecurityDepositAsPercent(), + alicesXmrAcct.getId(), + BSQ); + log.info("ORIGINAL XMR OFFER:\n{}", new TableBuilder(OFFER_TBL, originalOffer).build()); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + String newFixedPriceAsString = "0.000045"; + aliceClient.editOffer(originalOffer.getId(), + newFixedPriceAsString, + false, + 0.0, + 0, + DEACTIVATE_OFFER, + FIXED_PRICE_AND_ACTIVATION_STATE); + // Wait for edited offer to be edited and removed from offer-book. + genBtcBlocksThenWait(1, 2500); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED XMR OFFER:\n{}", new TableBuilder(OFFER_TBL, editedOffer).build()); + assertFalse(editedOffer.getIsActivated()); + assertEquals(scaledUpAltcoinOfferPrice.apply(newFixedPriceAsString), editedOffer.getPrice()); + assertMarketBasedPriceFieldsAreIgnored(editedOffer); + + doSanityCheck(originalOffer, editedOffer); } private OfferInfo createMktPricedOfferForEdit(String direction, @@ -612,6 +832,12 @@ private OfferInfo createFixedPricedOfferForEdit(String direction, BSQ); } + private void assertMarketBasedPriceFieldsAreIgnored(OfferInfo editedOffer) { + assertFalse(editedOffer.getUseMarketBasedPrice()); + assertEquals(0.00, editedOffer.getMarketPriceMargin()); + assertEquals(0, editedOffer.getTriggerPrice()); + } + private void doSanityCheck(OfferInfo originalOffer, OfferInfo editedOffer) { // Assert some of the immutable offer fields are unchanged. assertEquals(originalOffer.getDirection(), editedOffer.getDirection()); @@ -627,18 +853,18 @@ private void doSanityCheck(OfferInfo originalOffer, OfferInfo editedOffer) { assertEquals(originalOffer.getSellerSecurityDeposit(), editedOffer.getSellerSecurityDeposit()); } - private PaymentAccount getOrCreatePaymentAccount(String countryCode) { - if (paymentAcctCache.containsKey(countryCode)) { - return paymentAcctCache.get(countryCode); + private PaymentAccount getOrCreateFiatPaymentAccount(String countryCode) { + if (fiatPaymentAcctCache.containsKey(countryCode)) { + return fiatPaymentAcctCache.get(countryCode); } else { PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, countryCode); - paymentAcctCache.put(countryCode, paymentAcct); + fiatPaymentAcctCache.put(countryCode, paymentAcct); return paymentAcct; } } @AfterAll - public static void clearPaymentAcctCache() { - paymentAcctCache.clear(); + public static void clearFiatPaymentAcctCache() { + fiatPaymentAcctCache.clear(); } } 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 92b8e8309ca..9aabd6affb3 100644 --- a/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java @@ -4,6 +4,9 @@ import bisq.core.locale.FiatCurrency; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; +import bisq.core.payment.AssetAccount; +import bisq.core.payment.CryptoCurrencyAccount; +import bisq.core.payment.InstantCryptoCurrencyAccount; import bisq.core.payment.PaymentAccount; import com.google.gson.Gson; @@ -31,6 +34,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInfo; +import static bisq.core.payment.payload.PaymentMethod.BLOCK_CHAINS_ID; +import static bisq.core.payment.payload.PaymentMethod.BLOCK_CHAINS_INSTANT_ID; import static java.lang.String.format; import static java.lang.System.getProperty; import static java.nio.charset.StandardCharsets.UTF_8; @@ -224,19 +229,45 @@ protected final String getCommaDelimitedFiatCurrencyCodes(Collection tradeCurrencies) { return tradeCurrencies.stream() .sorted(Comparator.comparing(TradeCurrency::getCode)) // sorted by code - .map(c -> c.getCode()) + .map(TradeCurrency::getCode) .collect(Collectors.joining(",")); } protected final List getSwiftFormComments() { - List comments = new ArrayList<>(); - comments.addAll(PROPERTY_VALUE_JSON_COMMENTS); + List comments = new ArrayList<>(PROPERTY_VALUE_JSON_COMMENTS); // List wrappedSwiftComments = Res.getWrappedAsList("payment.swift.info", 110); // comments.addAll(wrappedSwiftComments); // comments.add("See https://bisq.wiki/SWIFT"); return comments; } + protected final void checkCryptoCurrencyAccount(CryptoCurrencyAccount cryptoCurrencyAccount, + String expectedAccountName, + String expectedTradeCurrencyCode, + String expectedAddress) { + assertEquals(BLOCK_CHAINS_ID, cryptoCurrencyAccount.getPaymentMethod().getId()); + checkAssetAccount(cryptoCurrencyAccount, expectedAccountName, expectedTradeCurrencyCode, expectedAddress); + } + + protected final void checkInstantCryptoCurrencyAccount(InstantCryptoCurrencyAccount instantCryptoAccount, + String expectedAccountName, + String expectedTradeCurrencyCode, + String expectedAddress) { + assertEquals(BLOCK_CHAINS_INSTANT_ID, instantCryptoAccount.getPaymentMethod().getId()); + checkAssetAccount(instantCryptoAccount, expectedAccountName, expectedTradeCurrencyCode, expectedAddress); + } + + protected final void checkAssetAccount(AssetAccount assetAccount, + String expectedAccountName, + String expectedTradeCurrencyCode, + String expectedAddress) { + assertEquals(expectedAccountName, assetAccount.getAccountName()); + assertEquals(1, assetAccount.getTradeCurrencies().size()); + assertNotNull(assetAccount.getSelectedTradeCurrency()); + assertEquals(expectedTradeCurrencyCode, assetAccount.getSelectedTradeCurrency().getCode()); + assertEquals(expectedAddress, assetAccount.getAddress()); + } + private File fillPaymentAccountForm(List comments) { File tmpJsonForm = null; try { 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 26e2030c844..695db065b2a 100644 --- a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java @@ -24,9 +24,11 @@ import bisq.core.payment.CapitualAccount; import bisq.core.payment.CashDepositAccount; import bisq.core.payment.ClearXchangeAccount; +import bisq.core.payment.CryptoCurrencyAccount; import bisq.core.payment.F2FAccount; import bisq.core.payment.FasterPaymentsAccount; import bisq.core.payment.HalCashAccount; +import bisq.core.payment.InstantCryptoCurrencyAccount; import bisq.core.payment.InteracETransferAccount; import bisq.core.payment.JapanBankAccount; import bisq.core.payment.MoneyBeamAccount; @@ -73,16 +75,23 @@ import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.ApiTestConfig.BSQ; +import static bisq.apitest.config.ApiTestConfig.EUR; +import static bisq.apitest.config.ApiTestConfig.RUBLE; +import static bisq.apitest.config.ApiTestConfig.XMR; import static bisq.apitest.config.BisqAppConfig.alicedaemon; -import static bisq.cli.TableFormat.formatPaymentAcctTbl; +import static bisq.cli.table.builder.TableType.PAYMENT_ACCOUNT_TBL; import static bisq.core.locale.CurrencyUtil.*; import static bisq.core.payment.payload.PaymentMethod.*; -import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + +import bisq.cli.table.builder.TableBuilder; + @SuppressWarnings({"OptionalGetWithoutIsPresent", "ConstantConditions"}) @Disabled @Slf4j @@ -98,6 +107,8 @@ public static void setUp() { } } + // Fiat Payment Accounts + @Test public void testCreateAdvancedCashAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, ADVANCED_CASH_ID); @@ -111,7 +122,7 @@ public void testCreateAdvancedCashAccount(TestInfo testInfo) { .stream() .map(TradeCurrency::getCode) .collect(Collectors.joining(","))); - COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "RUB"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, RUBLE); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Advanced Cash Acct Salt")); String jsonString = getCompletedFormAsJsonString(); AdvancedCashAccount paymentAccount = (AdvancedCashAccount) createPaymentAccount(aliceClient, jsonString); @@ -226,7 +237,7 @@ public void testCreateCashDepositAccount(TestInfo testInfo) { 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); @@ -378,7 +389,7 @@ public void testCreateHalCashAccount(TestInfo testInfo) { 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); @@ -463,7 +474,7 @@ public void testCreateMoneyBeamAccount(TestInfo testInfo) { 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()); @@ -712,7 +723,7 @@ public void testCreateSepaInstantAccount(TestInfo testInfo) { 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()); @@ -743,7 +754,7 @@ public void testCreateSepaAccount(TestInfo testInfo) { 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()); @@ -813,7 +824,7 @@ public void testCreateSwiftAccount(TestInfo testInfo) { COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "IT Swift Acct w/ DE Intermediary"); String allFiatCodes = getCommaDelimitedFiatCurrencyCodes(getAllSortedFiatCurrencies()); 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_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."); @@ -1107,6 +1118,76 @@ public void testCreateWesternUnionAccount(TestInfo testInfo) { print(paymentAccount); } + // AltCoin Payment Accounts + + @Test + public void testCreateBSQAccount(TestInfo testInfo) { + String bsqAddress = aliceClient.getUnusedBsqAddress(); + protobuf.PaymentAccount proto = + aliceClient.createCryptoCurrencyPaymentAccount(BSQ, + BSQ, + bsqAddress, + false); + CryptoCurrencyAccount cryptoAccount = toCryptoCurrencyAccount.apply(proto); + checkCryptoCurrencyAccount(cryptoAccount, BSQ, BSQ, bsqAddress); + print(cryptoAccount); + } + + @Test + public void testCreateBSQAccountWithInvalidAddressShouldThrowException(TestInfo testInfo) { + String bsqAddress = "BADADDRESSrmtxtzpwt25zq2pmeeu6qk8029w404ad0xn"; + Throwable exception = assertThrows(StatusRuntimeException.class, + () -> aliceClient.createCryptoCurrencyPaymentAccount(BSQ, BSQ, bsqAddress, false)); + assertEquals("INVALID_ARGUMENT: " + bsqAddress + " is not a valid bsq address", + exception.getMessage()); + } + + @Test + public void testCreateInstantBSQAccount(TestInfo testInfo) { + String bsqAddress = aliceClient.getUnusedBsqAddress(); + protobuf.PaymentAccount proto = + aliceClient.createCryptoCurrencyPaymentAccount(BSQ, BSQ, bsqAddress, true); + InstantCryptoCurrencyAccount instantCryptoAccount = toInstantCryptoCurrencyAccount.apply(proto); + checkInstantCryptoCurrencyAccount(instantCryptoAccount, BSQ, BSQ, bsqAddress); + print(instantCryptoAccount); + } + + @Test + public void testCreateXMRAccount(TestInfo testInfo) { + // Bisq does not support XMR$Testnet or XMR$Stagenet assets. + // All test XMR addresses must contain XMR$Mainnet prefixes. + String mainnetXMRAddress = + "4Azaj6jNxbx8Tu3GgN5wAxCmaVNx4LWPJBw9UGeb2cKoBkoUP9r7VQqAGjkn7QzzYNEzr7RJ3URhy3fy6C4BoKhB5UdtDuM"; + protobuf.PaymentAccount proto = + aliceClient.createCryptoCurrencyPaymentAccount(XMR, XMR, mainnetXMRAddress, false); + CryptoCurrencyAccount cryptoAccount = toCryptoCurrencyAccount.apply(proto); + checkCryptoCurrencyAccount(cryptoAccount, XMR, XMR, mainnetXMRAddress); + print(cryptoAccount); + } + + @Test + public void testCreateInstantXMRAccount(TestInfo testInfo) { + String mainnetXMRAddress = + "49nifJSHL4uPxKCMWseH6MacYHjw3a2Ly9Xs86w4xuUb61ufRbRSHR1BGad56K8xYNfpCCTyLYmxJWYxTtw6dD5HFsEvoLd"; + protobuf.PaymentAccount proto = + aliceClient.createCryptoCurrencyPaymentAccount(XMR, + XMR, + mainnetXMRAddress, + true); + InstantCryptoCurrencyAccount instantCryptoAccount = toInstantCryptoCurrencyAccount.apply(proto); + checkInstantCryptoCurrencyAccount(instantCryptoAccount, XMR, XMR, mainnetXMRAddress); + print(instantCryptoAccount); + } + + @Test + public void testCreateInstantXMRAccountWithInvalidAddressShouldThrowException(TestInfo testInfo) { + String mainnetXMRAddress = "BADADDRESSuPxKCMWseH6MacYHjw3a2Ly9Xs86w4xuUb61ufRbRSHR1BGad56K8xYNfpCCTyLYmxJWYxTtw6dD5HFsEvoLd"; + Throwable exception = assertThrows(StatusRuntimeException.class, + () -> aliceClient.createCryptoCurrencyPaymentAccount(XMR, XMR, mainnetXMRAddress, true)); + assertEquals("INVALID_ARGUMENT: " + mainnetXMRAddress + " is not a valid xmr address", + exception.getMessage()); + } + @AfterAll public static void tearDown() { tearDownScaffold(); @@ -1115,7 +1196,7 @@ public static void tearDown() { 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 4c4a6b34533..9d680e723d1 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java @@ -2,6 +2,8 @@ import bisq.proto.grpc.TradeInfo; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; import org.slf4j.Logger; @@ -9,16 +11,22 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.TestInfo; +import static bisq.apitest.config.ApiTestConfig.BTC; import static bisq.cli.CurrencyFormat.formatBsqAmount; -import static bisq.cli.TradeFormat.format; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; +import static bisq.cli.table.builder.TableType.TRADE_TBL; +import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; +import static bisq.core.trade.Trade.Phase.FIAT_SENT; +import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; +import static bisq.core.trade.Trade.State.BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG; +import static bisq.core.trade.Trade.State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN; +import static bisq.core.trade.Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG; +import static org.junit.jupiter.api.Assertions.*; import bisq.apitest.method.offer.AbstractOfferTest; import bisq.cli.GrpcClient; +import bisq.cli.table.builder.TableBuilder; public class AbstractTradeTest extends AbstractOfferTest { @@ -29,6 +37,8 @@ public class AbstractTradeTest extends AbstractOfferTest { protected final Supplier maxTradeStateAndPhaseChecks = () -> isLongRunningTest ? 10 : 2; + private final Function toUserName = (client) -> client.equals(aliceClient) ? "Alice" : "Bob"; + @BeforeAll public static void initStaticFixtures() { EXPECTED_PROTOCOL_STATUS.init(); @@ -37,14 +47,138 @@ public static void initStaticFixtures() { protected final TradeInfo takeAlicesOffer(String offerId, String paymentAccountId, String takerFeeCurrencyCode) { - return bobClient.takeOffer(offerId, paymentAccountId, takerFeeCurrencyCode); + return takeAlicesOffer(offerId, + paymentAccountId, + takerFeeCurrencyCode, + true); + } + + protected final TradeInfo takeAlicesOffer(String offerId, + String paymentAccountId, + String takerFeeCurrencyCode, + boolean generateBtcBlock) { + @SuppressWarnings("ConstantConditions") + var trade = bobClient.takeOffer(offerId, + paymentAccountId, + takerFeeCurrencyCode); + assertNotNull(trade); + assertEquals(offerId, trade.getTradeId()); + + if (takerFeeCurrencyCode.equals(BTC)) + assertTrue(trade.getIsCurrencyForTakerFeeBtc()); + else + assertFalse(trade.getIsCurrencyForTakerFeeBtc()); + + // Cache the trade id for the other tests. + tradeId = trade.getTradeId(); + + if (generateBtcBlock) + genBtcBlocksThenWait(1, 6_000); + + return trade; } - @SuppressWarnings("unused") - protected final TradeInfo takeBobsOffer(String offerId, - String paymentAccountId, - String takerFeeCurrencyCode) { - return aliceClient.takeOffer(offerId, paymentAccountId, takerFeeCurrencyCode); + + protected final void waitForDepositConfirmation(Logger log, + TestInfo testInfo, + GrpcClient grpcClient, + String tradeId) { + Predicate isTradeInDepositConfirmedStateAndPhase = (t) -> + t.getState().equals(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN.name()) + && t.getPhase().equals(DEPOSIT_CONFIRMED.name()); + + String userName = toUserName.apply(grpcClient); + for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { + TradeInfo trade = grpcClient.getTrade(tradeId); + if (!isTradeInDepositConfirmedStateAndPhase.test(trade)) { + log.warn("{} still waiting on trade {} tx {}: DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, attempt # {}", + userName, + trade.getShortId(), + trade.getDepositTxId(), + i); + genBtcBlocksThenWait(1, 4_000); + } else { + EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN) + .setPhase(DEPOSIT_CONFIRMED) + .setDepositPublished(true) + .setDepositConfirmed(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, + testInfo, + userName + "'s view after deposit is confirmed", + trade, + true); + break; + } + } + } + + protected final void verifyTakerDepositConfirmed(TradeInfo trade) { + if (!trade.getIsDepositConfirmed()) { + fail(String.format("INVALID_PHASE for trade %s in STATE=%s PHASE=%s, deposit tx never confirmed.", + 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.getIsFiatSent()) { + log.warn("{} still waiting for trade {} {}, attempt # {}", + userName, + trade.getShortId(), + BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG, + i); + sleep(5_000); + } else { + // Do not check trade.getOffer().getState() here because + // it might be AVAILABLE, not OFFER_FEE_PAID. + EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG) + .setPhase(FIAT_SENT) + .setFiatSent(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_FIAT_PAYMENT_INITIATED_MSG.name()) && + (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(FIAT_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(String.format("INVALID_PHASE for {}'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) { @@ -96,6 +230,17 @@ protected final void verifyBsqPaymentHasBeenReceived(Logger log, trade.getTradeId())); } + protected final void logBalances(Logger log, TestInfo testInfo) { + 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)); + } + protected final void logTrade(Logger log, TestInfo testInfo, String description, @@ -112,12 +257,12 @@ protected final void logTrade(Logger log, log.info(String.format("%s %s%n%s", testName(testInfo), description.toUpperCase(), - format(trade))); + new TableBuilder(TRADE_TBL, trade).build())); else if (log.isDebugEnabled()) { log.debug(String.format("%s %s%n%s", testName(testInfo), description.toUpperCase(), - format(trade))); + new TableBuilder(TRADE_TBL, trade).build())); } } } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBSQOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBSQOfferTest.java index fc365931d5f..2d59d1ce127 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBSQOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBSQOfferTest.java @@ -17,12 +17,8 @@ package bisq.apitest.method.trade; -import bisq.proto.grpc.TradeInfo; - import io.grpc.StatusRuntimeException; -import java.util.function.Predicate; - import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeAll; @@ -34,21 +30,13 @@ import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.config.ApiTestConfig.BSQ; -import static bisq.cli.TableFormat.formatBalancesTbls; -import static bisq.cli.TableFormat.formatOfferTable; +import static bisq.cli.table.builder.TableType.OFFER_TBL; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; -import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; -import static bisq.core.trade.Trade.Phase.FIAT_SENT; import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; import static bisq.core.trade.Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG; -import static bisq.core.trade.Trade.State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN; -import static bisq.core.trade.Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG; import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; -import static java.lang.String.format; -import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; import static protobuf.Offer.State.OFFER_FEE_PAID; import static protobuf.OfferPayload.Direction.SELL; @@ -56,8 +44,7 @@ import bisq.apitest.method.offer.AbstractOfferTest; - -// https://github.com/ghubstan/bisq/blob/master/cli/src/main/java/bisq/cli/TradeFormat.java +import bisq.cli.table.builder.TableBuilder; @Disabled @Slf4j @@ -79,6 +66,7 @@ public static void setUp() { @Test @Order(1) public void testTakeAlicesSellBTCForBSQOffer(final TestInfo testInfo) { + log.info("Bob's Balance @ Test Start :\n{}", formatBalancesTbls(bobClient.getBalances())); try { // Alice is going to BUY BSQ, but the Offer direction = SELL because it is a // BTC trade; Alice will SELL BTC for BSQ. Bob will send Alice BSQ. @@ -92,7 +80,7 @@ public void testTakeAlicesSellBTCForBSQOffer(final TestInfo testInfo) { getDefaultBuyerSecurityDepositAsPercent(), alicesBsqAcct.getId(), TRADE_FEE_CURRENCY_CODE); - log.info("ALICE'S BUY BSQ (SELL BTC) OFFER:\n{}", formatOfferTable(singletonList(alicesOffer), BSQ)); + log.info("ALICE'S BUY BSQ (SELL BTC) OFFER:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); genBtcBlocksThenWait(1, 5000); var offerId = alicesOffer.getId(); assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc()); @@ -101,49 +89,21 @@ public void testTakeAlicesSellBTCForBSQOffer(final TestInfo testInfo) { assertEquals(1, alicesBsqOffers.size()); var trade = takeAlicesOffer(offerId, bobsBsqAcct.getId(), TRADE_FEE_CURRENCY_CODE); - assertNotNull(trade); - assertEquals(offerId, trade.getTradeId()); - assertFalse(trade.getIsCurrencyForTakerFeeBtc()); - // Cache the trade id for the other tests. - tradeId = trade.getTradeId(); - - genBtcBlocksThenWait(1, 6000); - alicesBsqOffers = aliceClient.getMyBsqOffersSortedByDate(); + + alicesBsqOffers = aliceClient.getMyCryptoCurrencyOffersSortedByDate(BSQ); assertEquals(0, alicesBsqOffers.size()); - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - trade = bobClient.getTrade(trade.getTradeId()); - - if (!trade.getIsDepositConfirmed()) { - log.warn("Bob still waiting on trade {} tx {}: DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, attempt # {}", - trade.getShortId(), - trade.getDepositTxId(), - i); - genBtcBlocksThenWait(1, 4000); - continue; - } else { - EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN) - .setPhase(DEPOSIT_CONFIRMED) - .setDepositPublished(true) - .setDepositConfirmed(true); - verifyExpectedProtocolStatus(trade); - logTrade(log, testInfo, "Bob's view after taking offer and deposit confirmed", trade); - break; - } - } - - genBtcBlocksThenWait(1, 2500); - - if (!trade.getIsDepositConfirmed()) { - fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, deposit tx was never confirmed.", - trade.getShortId(), - trade.getState(), - trade.getPhase())); - } + genBtcBlocksThenWait(1, 2_500); + + waitForDepositConfirmation(log, testInfo, bobClient, trade.getTradeId()); + // TODO Find out why I have to gen btc blk here to avoid insufficient funds error. + genBtcBlocksThenWait(1, 2_500); + + trade = bobClient.getTrade(tradeId); + verifyTakerDepositConfirmed(trade); logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId), true); logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId), true); - } catch (StatusRuntimeException e) { fail(e); } @@ -155,59 +115,18 @@ public void testBobsConfirmPaymentStarted(final TestInfo testInfo) { try { var trade = bobClient.getTrade(tradeId); - Predicate tradeStateAndPhaseCorrect = (t) -> - t.getState().equals(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN.name()) - && t.getPhase().equals(DEPOSIT_CONFIRMED.name()); - - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - if (!tradeStateAndPhaseCorrect.test(trade)) { - log.warn("INVALID_PHASE for Bob's trade {} in STATE={} PHASE={}, cannot send payment started msg yet.", - trade.getShortId(), - trade.getState(), - trade.getPhase()); - sleep(10_000); - 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 send payment started msg.", - trade.getShortId(), - trade.getState(), - trade.getPhase())); - } + verifyTakerDepositConfirmed(trade); sendBsqPayment(log, bobClient, trade); - genBtcBlocksThenWait(1, 2500); + genBtcBlocksThenWait(1, 6_000); + bobClient.confirmPaymentStarted(trade.getTradeId()); - sleep(6000); - - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - trade = aliceClient.getTrade(tradeId); - - if (!trade.getIsFiatSent()) { - log.warn("Alice still waiting for trade {} SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG, attempt # {}", - trade.getShortId(), - i); - sleep(5000); - continue; - } else { - // Warning: trade.getOffer().getState() might be AVAILABLE, not OFFER_FEE_PAID. - EXPECTED_PROTOCOL_STATUS.setState(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG) - .setPhase(FIAT_SENT) - .setFiatSent(true); - verifyExpectedProtocolStatus(trade); - logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade); - break; - } - } + sleep(6_000); + + waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId); logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Sent)", aliceClient.getTrade(tradeId), true); logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Sent)", bobClient.getTrade(tradeId), true); - } catch (StatusRuntimeException e) { fail(e); } @@ -217,38 +136,15 @@ public void testBobsConfirmPaymentStarted(final TestInfo testInfo) { @Order(3) public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) { try { + waitForSellerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId); + + sleep(2_000); var trade = aliceClient.getTrade(tradeId); - Predicate tradeStateAndPhaseCorrect = (t) -> - t.getState().equals(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG.name()) - && (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(FIAT_SENT.name())); - - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - if (!tradeStateAndPhaseCorrect.test(trade)) { - log.warn("INVALID_PHASE for Alice's trade {} in STATE={} PHASE={}, cannot confirm payment received yet.", - trade.getShortId(), - trade.getState(), - trade.getPhase()); - 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, cannot confirm payment received.", - trade.getShortId(), - trade.getState(), - trade.getPhase())); - } - - sleep(2000); verifyBsqPaymentHasBeenReceived(log, aliceClient, trade); aliceClient.confirmPaymentReceived(trade.getTradeId()); - sleep(3000); + sleep(3_000); trade = aliceClient.getTrade(tradeId); assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); @@ -257,11 +153,8 @@ public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) { .setPayoutPublished(true) .setFiatReceived(true); verifyExpectedProtocolStatus(trade); - logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade); - logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Received)", aliceClient.getTrade(tradeId), true); logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Received)", bobClient.getTrade(tradeId), true); - } catch (StatusRuntimeException e) { fail(e); } @@ -271,32 +164,21 @@ public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) { @Order(4) public void testBobsKeepFunds(final TestInfo testInfo) { try { - genBtcBlocksThenWait(1, 1000); + genBtcBlocksThenWait(1, 1_000); var trade = bobClient.getTrade(tradeId); logTrade(log, testInfo, "Alice's view before keeping funds", trade); bobClient.keepFunds(tradeId); - genBtcBlocksThenWait(1, 1000); + genBtcBlocksThenWait(1, 1_000); trade = bobClient.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)); - + logBalances(log, testInfo); } catch (StatusRuntimeException e) { fail(e); } 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 8f03520b525..597673adcfd 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 @@ 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,18 +31,13 @@ import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.config.ApiTestConfig.BSQ; -import static bisq.cli.TableFormat.formatBalancesTbls; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; -import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; -import static bisq.core.trade.Trade.Phase.FIAT_SENT; import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; -import static bisq.core.trade.Trade.State.*; -import static java.lang.String.format; +import static 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.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; -import static protobuf.Offer.State.OFFER_FEE_PAID; import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OpenOffer.State.AVAILABLE; @@ -78,53 +69,26 @@ public void testTakeAlicesBuyOffer(final TestInfo testInfo) { assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc()); // 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 + // Wait times vary; my logs show >= 2-second delay. + sleep(3_000); // TODO loop instead of hard code wait time var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), "usd"); assertEquals(1, alicesUsdOffers.size()); PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US"); + var trade = takeAlicesOffer(offerId, bobsUsdAccount.getId(), TRADE_FEE_CURRENCY_CODE); - assertNotNull(trade); - assertEquals(offerId, trade.getTradeId()); - assertFalse(trade.getIsCurrencyForTakerFeeBtc()); - // Cache the trade id for the other tests. - tradeId = trade.getTradeId(); - genBtcBlocksThenWait(1, 4000); alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), "usd"); assertEquals(0, alicesUsdOffers.size()); - genBtcBlocksThenWait(1, 2500); - - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - trade = bobClient.getTrade(trade.getTradeId()); - - if (!trade.getIsDepositConfirmed()) { - log.warn("Bob still waiting on trade {} tx {}: DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, attempt # {}", - trade.getShortId(), - trade.getDepositTxId(), - i); - genBtcBlocksThenWait(1, 4000); - continue; - } else { - EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN) - .setPhase(DEPOSIT_CONFIRMED) - .setDepositPublished(true) - .setDepositConfirmed(true); - verifyExpectedProtocolStatus(trade); - logTrade(log, testInfo, "Bob's view after deposit is confirmed", trade, true); - break; - } - } + genBtcBlocksThenWait(1, 2_500); - if (!trade.getIsDepositConfirmed()) { - fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, deposit tx was never confirmed.", - trade.getShortId(), - trade.getState(), - trade.getPhase())); - } + waitForDepositConfirmation(log, testInfo, bobClient, trade.getTradeId()); + trade = bobClient.getTrade(tradeId); + verifyTakerDepositConfirmed(trade); + logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId), true); } catch (StatusRuntimeException e) { fail(e); } @@ -136,55 +100,12 @@ public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) { try { var trade = aliceClient.getTrade(tradeId); - Predicate tradeStateAndPhaseCorrect = (t) -> - t.getState().equals(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN.name()) - && t.getPhase().equals(DEPOSIT_CONFIRMED.name()); - - - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - if (!tradeStateAndPhaseCorrect.test(trade)) { - log.warn("INVALID_PHASE for Alice's trade {} in STATE={} PHASE={}, cannot confirm payment started yet.", - trade.getShortId(), - trade.getState(), - trade.getPhase()); - // fail("Bad trade state and phase."); - sleep(1000 * 10); - trade = aliceClient.getTrade(tradeId); - continue; - } else { - break; - } - } - - if (!tradeStateAndPhaseCorrect.test(trade)) { - fail(format("INVALID_PHASE for Alice's trade %s in STATE=%s PHASE=%s, could not confirm payment started.", - trade.getShortId(), - trade.getState(), - trade.getPhase())); - } + waitForDepositConfirmation(log, testInfo, aliceClient, trade.getTradeId()); aliceClient.confirmPaymentStarted(trade.getTradeId()); - sleep(6000); + sleep(6_000); - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - trade = aliceClient.getTrade(tradeId); - - if (!trade.getIsFiatSent()) { - log.warn("Alice still waiting for trade {} BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG, attempt # {}", - trade.getShortId(), - i); - sleep(5000); - continue; - } else { - assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); - EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG) - .setPhase(FIAT_SENT) - .setFiatSent(true); - verifyExpectedProtocolStatus(trade); - logTrade(log, testInfo, "Alice's view after confirming fiat payment sent", trade); - break; - } - } + waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId); } catch (StatusRuntimeException e) { fail(e); } @@ -194,36 +115,11 @@ public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) { @Order(3) public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { try { - var trade = bobClient.getTrade(tradeId); - - Predicate tradeStateAndPhaseCorrect = (t) -> - t.getState().equals(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG.name()) - && (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(FIAT_SENT.name())); - - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - if (!tradeStateAndPhaseCorrect.test(trade)) { - log.warn("INVALID_PHASE for Bob's trade {} in STATE={} PHASE={}, cannot confirm payment received yet.", - trade.getShortId(), - trade.getState(), - trade.getPhase()); - // fail("Bad trade state and phase."); - sleep(1000 * 10); - trade = bobClient.getTrade(tradeId); - continue; - } else { - break; - } - } - - if (!tradeStateAndPhaseCorrect.test(trade)) { - fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, cannot confirm payment received.", - trade.getShortId(), - trade.getState(), - trade.getPhase())); - } + waitForSellerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId); + var trade = bobClient.getTrade(tradeId); bobClient.confirmPaymentReceived(trade.getTradeId()); - sleep(3000); + sleep(3_000); trade = bobClient.getTrade(tradeId); // Note: offer.state == available @@ -234,7 +130,6 @@ public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { .setFiatReceived(true); verifyExpectedProtocolStatus(trade); logTrade(log, testInfo, "Bob's view after confirming fiat payment received", trade); - } catch (StatusRuntimeException e) { fail(e); } @@ -244,32 +139,22 @@ public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { @Order(4) public void testAlicesKeepFunds(final TestInfo testInfo) { try { - genBtcBlocksThenWait(1, 1000); + genBtcBlocksThenWait(1, 1_000); var trade = aliceClient.getTrade(tradeId); logTrade(log, testInfo, "Alice's view before keeping funds", trade); aliceClient.keepFunds(tradeId); - genBtcBlocksThenWait(1, 1000); + genBtcBlocksThenWait(1, 1_000); 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)); + logBalances(log, testInfo); } 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 index 1035875010e..c577b92c418 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java @@ -37,12 +37,8 @@ import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.NationalBankAccountPayload; -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; @@ -53,13 +49,10 @@ import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.config.ApiTestConfig.BSQ; -import static bisq.cli.TableFormat.formatBalancesTbls; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; -import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; -import static bisq.core.trade.Trade.Phase.FIAT_SENT; import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; -import static bisq.core.trade.Trade.State.*; -import static java.lang.String.format; +import static 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.*; import static protobuf.Offer.State.OFFER_FEE_PAID; import static protobuf.OfferPayload.Direction.BUY; @@ -110,17 +103,15 @@ public void testTakeAlicesBuyOffer(final TestInfo testInfo) { // 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 + 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(), TRADE_FEE_CURRENCY_CODE); - assertNotNull(trade); - assertEquals(offerId, trade.getTradeId()); - assertFalse(trade.getIsCurrencyForTakerFeeBtc()); - // Cache the trade id for the other tests. - tradeId = trade.getTradeId(); + var trade = takeAlicesOffer(offerId, + bobsPaymentAccount.getId(), + TRADE_FEE_CURRENCY_CODE, + false); // Before generating a blk and confirming deposit tx, make sure there // are no bank acct details in the either side's contract. @@ -149,35 +140,14 @@ public void testTakeAlicesBuyOffer(final TestInfo testInfo) { alicesOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), BRL); assertEquals(0, alicesOffers.size()); - genBtcBlocksThenWait(1, 2500); - - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - trade = bobClient.getTrade(trade.getTradeId()); - - if (!trade.getIsDepositConfirmed()) { - log.warn("Bob still waiting on trade {} tx {}: DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, attempt # {}", - trade.getShortId(), - trade.getDepositTxId(), - i); - genBtcBlocksThenWait(1, 4000); - } else { - EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN) - .setPhase(DEPOSIT_CONFIRMED) - .setDepositPublished(true) - .setDepositConfirmed(true); - verifyExpectedProtocolStatus(trade); - logTrade(log, testInfo, "Bob's view after deposit is confirmed", trade, true); - break; - } - } + genBtcBlocksThenWait(1, 2_500); - if (!trade.getIsDepositConfirmed()) { - fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, deposit tx was never confirmed.", - trade.getShortId(), - trade.getState(), - trade.getPhase())); - } + waitForDepositConfirmation(log, testInfo, bobClient, trade.getTradeId()); + trade = bobClient.getTrade(tradeId); + verifyTakerDepositConfirmed(trade); + logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId), true); } catch (StatusRuntimeException e) { fail(e); } @@ -204,52 +174,18 @@ public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) { try { var trade = aliceClient.getTrade(tradeId); - Predicate tradeStateAndPhaseCorrect = (t) -> - t.getState().equals(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN.name()) - && t.getPhase().equals(DEPOSIT_CONFIRMED.name()); + waitForDepositConfirmation(log, testInfo, aliceClient, trade.getTradeId()); + aliceClient.confirmPaymentStarted(trade.getTradeId()); + sleep(6_000); - 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()); - sleep(1000 * 10); - trade = aliceClient.getTrade(tradeId); - } else { - break; - } - } + waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId); - 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())); - } + trade = aliceClient.getTrade(tradeId); + assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); - aliceClient.confirmPaymentStarted(trade.getTradeId()); - sleep(6000); - - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - trade = aliceClient.getTrade(tradeId); - - if (!trade.getIsFiatSent()) { - log.warn("Alice still waiting for trade {} BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG, attempt # {}", - trade.getShortId(), - i); - sleep(5000); - } else { - assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); - EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG) - .setPhase(FIAT_SENT) - .setFiatSent(true); - verifyExpectedProtocolStatus(trade); - logTrade(log, testInfo, "Alice's view after confirming fiat payment sent", trade); - break; - } - } + logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Sent)", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Sent)", bobClient.getTrade(tradeId), true); } catch (StatusRuntimeException e) { fail(e); } @@ -259,34 +195,11 @@ public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) { @Order(4) public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { try { - var trade = bobClient.getTrade(tradeId); - - Predicate tradeStateAndPhaseCorrect = (t) -> - t.getState().equals(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG.name()) - && (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(FIAT_SENT.name())); - - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - if (!tradeStateAndPhaseCorrect.test(trade)) { - log.warn("INVALID_PHASE for Bob's trade {} in STATE={} PHASE={}, cannot confirm payment received yet.", - trade.getShortId(), - trade.getState(), - trade.getPhase()); - sleep(1000 * 10); - trade = bobClient.getTrade(tradeId); - } 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())); - } + waitForSellerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId); + var trade = bobClient.getTrade(tradeId); bobClient.confirmPaymentReceived(trade.getTradeId()); - sleep(3000); + sleep(3_000); trade = bobClient.getTrade(tradeId); // Note: offer.state == available @@ -297,7 +210,6 @@ public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { .setFiatReceived(true); verifyExpectedProtocolStatus(trade); logTrade(log, testInfo, "Bob's view after confirming fiat payment received", trade); - } catch (StatusRuntimeException e) { fail(e); } @@ -307,32 +219,22 @@ public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { @Order(5) public void testAlicesKeepFunds(final TestInfo testInfo) { try { - genBtcBlocksThenWait(1, 1000); + genBtcBlocksThenWait(1, 1_000); var trade = aliceClient.getTrade(tradeId); logTrade(log, testInfo, "Alice's view before keeping funds", trade); aliceClient.keepFunds(tradeId); - genBtcBlocksThenWait(1, 1000); + genBtcBlocksThenWait(1, 1_000); 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)); + logBalances(log, testInfo); } catch (StatusRuntimeException e) { fail(e); } 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 00000000000..15dabab129e --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyXMROfferTest.java @@ -0,0 +1,184 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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.BSQ; +import static bisq.apitest.config.ApiTestConfig.XMR; +import static bisq.cli.table.builder.TableType.OFFER_TBL; +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; +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.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; +import static protobuf.Offer.State.OFFER_FEE_PAID; +import static protobuf.OfferPayload.Direction.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). + + // Maker and Taker fees are in BSQ. + private static final String TRADE_FEE_CURRENCY_CODE = BSQ; + + @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 + getDefaultBuyerSecurityDepositAsPercent(), + alicesXmrAcct.getId(), + TRADE_FEE_CURRENCY_CODE); + log.info("ALICE'S BUY XMR (SELL BTC) OFFER:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); + genBtcBlocksThenWait(1, 5000); + var offerId = alicesOffer.getId(); + assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc()); + + var alicesXmrOffers = aliceClient.getMyCryptoCurrencyOffers(btcTradeDirection, XMR); + assertEquals(1, alicesXmrOffers.size()); + + var trade = takeAlicesOffer(offerId, bobsXmrAcct.getId(), TRADE_FEE_CURRENCY_CODE); + + alicesXmrOffers = aliceClient.getMyCryptoCurrencyOffersSortedByDate(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), true); + logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId), true); + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(2) + public void testBobsConfirmPaymentStarted(final TestInfo testInfo) { + try { + var trade = bobClient.getTrade(tradeId); + + verifyTakerDepositConfirmed(trade); + + log.info("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), true); + logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Sent)", bobClient.getTrade(tradeId), true); + } 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 / BSQ wallet, but we can do no such checks for XMR payments. + // All XMR transfers are done outside Bisq. + log.info("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_PAID.name(), trade.getOffer().getState()); + EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) + .setPhase(PAYOUT_PUBLISHED) + .setPayoutPublished(true) + .setFiatReceived(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Received)", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Received)", bobClient.getTrade(tradeId), true); + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(4) + public void testBobsKeepFunds(final TestInfo testInfo) { + try { + genBtcBlocksThenWait(1, 1_000); + + var trade = bobClient.getTrade(tradeId); + logTrade(log, testInfo, "Alice's view before keeping funds", trade); + + bobClient.keepFunds(tradeId); + genBtcBlocksThenWait(1, 1_000); + + trade = bobClient.getTrade(tradeId); + EXPECTED_PROTOCOL_STATUS.setState(BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG) + .setPhase(PAYOUT_PUBLISHED); + verifyExpectedProtocolStatus(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); + logBalances(log, testInfo); + } catch (StatusRuntimeException e) { + fail(e); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBSQOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBSQOfferTest.java index 786601e6fa1..518c62c8613 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBSQOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBSQOfferTest.java @@ -17,12 +17,8 @@ package bisq.apitest.method.trade; -import bisq.proto.grpc.TradeInfo; - import io.grpc.StatusRuntimeException; -import java.util.function.Predicate; - import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeAll; @@ -35,21 +31,13 @@ import static bisq.apitest.config.ApiTestConfig.BSQ; import static bisq.apitest.config.ApiTestConfig.BTC; -import static bisq.cli.TableFormat.formatBalancesTbls; -import static bisq.cli.TableFormat.formatOfferTable; +import static bisq.cli.table.builder.TableType.OFFER_TBL; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; -import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; -import static bisq.core.trade.Trade.Phase.FIAT_SENT; import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; import static bisq.core.trade.Trade.Phase.WITHDRAWN; -import static bisq.core.trade.Trade.State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN; -import static bisq.core.trade.Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG; 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 java.lang.String.format; -import static java.util.Collections.singletonList; 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.OfferPayload.Direction.BUY; @@ -57,6 +45,7 @@ import bisq.apitest.method.offer.AbstractOfferTest; +import bisq.cli.table.builder.TableBuilder; @Disabled @Slf4j @@ -93,7 +82,7 @@ public void testTakeAlicesBuyBTCForBSQOffer(final TestInfo testInfo) { getDefaultBuyerSecurityDepositAsPercent(), alicesBsqAcct.getId(), TRADE_FEE_CURRENCY_CODE); - log.info("ALICE'S SELL BSQ (BUY BTC) OFFER:\n{}", formatOfferTable(singletonList(alicesOffer), BSQ)); + log.info("ALICE'S SELL BSQ (BUY BTC) OFFER:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); genBtcBlocksThenWait(1, 4000); var offerId = alicesOffer.getId(); assertTrue(alicesOffer.getIsCurrencyForMakerFeeBtc()); @@ -102,49 +91,18 @@ public void testTakeAlicesBuyBTCForBSQOffer(final TestInfo testInfo) { assertEquals(1, alicesBsqOffers.size()); var trade = takeAlicesOffer(offerId, bobsBsqAcct.getId(), TRADE_FEE_CURRENCY_CODE); - assertNotNull(trade); - assertEquals(offerId, trade.getTradeId()); - assertTrue(trade.getIsCurrencyForTakerFeeBtc()); - // Cache the trade id for the other tests. - tradeId = trade.getTradeId(); - - genBtcBlocksThenWait(1, 6000); - alicesBsqOffers = aliceClient.getMyBsqOffersSortedByDate(); + + alicesBsqOffers = aliceClient.getMyCryptoCurrencyOffersSortedByDate(BSQ); assertEquals(0, alicesBsqOffers.size()); - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - trade = bobClient.getTrade(trade.getTradeId()); - - if (!trade.getIsDepositConfirmed()) { - log.warn("Bob still waiting on trade {} tx {}: DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, attempt # {}", - trade.getShortId(), - trade.getDepositTxId(), - i); - genBtcBlocksThenWait(1, 4000); - continue; - } else { - EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN) - .setPhase(DEPOSIT_CONFIRMED) - .setDepositPublished(true) - .setDepositConfirmed(true); - verifyExpectedProtocolStatus(trade); - logTrade(log, testInfo, "Bob's view after taking offer and deposit confirmed", trade); - break; - } - } - - genBtcBlocksThenWait(1, 2500); - - if (!trade.getIsDepositConfirmed()) { - fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, deposit tx was never confirmed.", - trade.getShortId(), - trade.getState(), - trade.getPhase())); - } + genBtcBlocksThenWait(1, 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), true); logTrade(log, testInfo, "Bob's Taker/Buyer View", bobClient.getTrade(tradeId), true); - } catch (StatusRuntimeException e) { fail(e); } @@ -156,59 +114,17 @@ public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) { try { var trade = aliceClient.getTrade(tradeId); - Predicate tradeStateAndPhaseCorrect = (t) -> - t.getState().equals(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN.name()) - && t.getPhase().equals(DEPOSIT_CONFIRMED.name()); - - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - if (!tradeStateAndPhaseCorrect.test(trade)) { - log.warn("INVALID_PHASE for Alice's trade {} in STATE={} PHASE={}, cannot send payment started msg yet.", - trade.getShortId(), - trade.getState(), - trade.getPhase()); - sleep(10_000); - 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 send payment started msg.", - trade.getShortId(), - trade.getState(), - trade.getPhase())); - } + waitForDepositConfirmation(log, testInfo, aliceClient, trade.getTradeId()); sendBsqPayment(log, aliceClient, trade); - genBtcBlocksThenWait(1, 2500); + genBtcBlocksThenWait(1, 2_500); aliceClient.confirmPaymentStarted(trade.getTradeId()); - sleep(6000); - - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - trade = bobClient.getTrade(tradeId); - - if (!trade.getIsFiatSent()) { - log.warn("Bob still waiting for trade {} SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG, attempt # {}", - trade.getShortId(), - i); - sleep(5000); - continue; - } else { - // Warning: trade.getOffer().getState() might be AVAILABLE, not OFFER_FEE_PAID. - EXPECTED_PROTOCOL_STATUS.setState(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG) - .setPhase(FIAT_SENT) - .setFiatSent(true); - verifyExpectedProtocolStatus(trade); - logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade); - break; - } - } + sleep(6_000); + + waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId); logTrade(log, testInfo, "Alice's Maker/Seller View (Payment Sent)", aliceClient.getTrade(tradeId), true); logTrade(log, testInfo, "Bob's Taker/Buyer View (Payment Sent)", bobClient.getTrade(tradeId), true); - } catch (StatusRuntimeException e) { fail(e); } @@ -218,38 +134,14 @@ public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) { @Order(3) public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { try { - var trade = bobClient.getTrade(tradeId); + waitForSellerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId); - Predicate tradeStateAndPhaseCorrect = (t) -> - t.getState().equals(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG.name()) - && (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(FIAT_SENT.name())); - - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - if (!tradeStateAndPhaseCorrect.test(trade)) { - log.warn("INVALID_PHASE for Bob's trade {} in STATE={} PHASE={}, cannot confirm payment received yet.", - trade.getShortId(), - trade.getState(), - trade.getPhase()); - 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())); - } - - sleep(2000); + sleep(2_000); + var trade = bobClient.getTrade(tradeId); verifyBsqPaymentHasBeenReceived(log, bobClient, trade); bobClient.confirmPaymentReceived(trade.getTradeId()); - sleep(3000); + sleep(3_000); trade = bobClient.getTrade(tradeId); // Warning: trade.getOffer().getState() might be AVAILABLE, not OFFER_FEE_PAID. @@ -258,11 +150,8 @@ public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { .setPayoutPublished(true) .setFiatReceived(true); verifyExpectedProtocolStatus(trade); - logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade); - logTrade(log, testInfo, "Alice's Maker/Seller View (Payment Received)", aliceClient.getTrade(tradeId), true); logTrade(log, testInfo, "Bob's Taker/Buyer View (Payment Received)", bobClient.getTrade(tradeId), true); - } catch (StatusRuntimeException e) { fail(e); } @@ -272,7 +161,7 @@ public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { @Order(4) public void testAlicesBtcWithdrawalToExternalAddress(final TestInfo testInfo) { try { - genBtcBlocksThenWait(1, 1000); + genBtcBlocksThenWait(1, 1_000); var trade = aliceClient.getTrade(tradeId); logTrade(log, testInfo, "Alice's view before withdrawing BTC funds to external wallet", trade); @@ -280,28 +169,16 @@ public void testAlicesBtcWithdrawalToExternalAddress(final TestInfo testInfo) { String toAddress = bitcoinCli.getNewBtcAddress(); aliceClient.withdrawFunds(tradeId, toAddress, WITHDRAWAL_TX_MEMO); - genBtcBlocksThenWait(1, 1000); + genBtcBlocksThenWait(1, 1_000); trade = aliceClient.getTrade(tradeId); EXPECTED_PROTOCOL_STATUS.setState(WITHDRAW_COMPLETED) .setPhase(WITHDRAWN) .setWithdrawn(true); verifyExpectedProtocolStatus(trade); - logTrade(log, testInfo, "Alice's view after withdrawing funds to external wallet", trade); - - logTrade(log, testInfo, "Alice's Maker/Seller View (Done)", aliceClient.getTrade(tradeId), true); logTrade(log, testInfo, "Bob's Taker/Buyer 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)); - + logBalances(log, testInfo); } 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 c4abd90934b..380a7818e7a 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 @@ 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,21 +31,16 @@ 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_CONFIRMED; -import static bisq.core.trade.Trade.Phase.FIAT_SENT; 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_PAID; import static protobuf.OfferPayload.Direction.SELL; -import static protobuf.OpenOffer.State.AVAILABLE; @Disabled @Slf4j @@ -83,52 +74,24 @@ public void testTakeAlicesSellOffer(final TestInfo testInfo) { // Wait for Alice's AddToOfferBook task. // 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 + sleep(3_000); // TODO loop instead of hard code wait time var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(SELL.name(), "usd"); assertEquals(1, alicesUsdOffers.size()); PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US"); var trade = takeAlicesOffer(offerId, bobsUsdAccount.getId(), TRADE_FEE_CURRENCY_CODE); - assertNotNull(trade); - assertEquals(offerId, trade.getTradeId()); - assertTrue(trade.getIsCurrencyForTakerFeeBtc()); - // Cache the trade id for the other tests. - tradeId = trade.getTradeId(); - genBtcBlocksThenWait(1, 4000); 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.getIsDepositConfirmed()) { - log.warn("Bob still waiting on trade {} tx {}: DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, attempt # {}", - trade.getShortId(), - trade.getDepositTxId(), - i); - genBtcBlocksThenWait(1, 4000); - continue; - } else { - EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN) - .setPhase(DEPOSIT_CONFIRMED) - .setDepositPublished(true) - .setDepositConfirmed(true); - verifyExpectedProtocolStatus(trade); - logTrade(log, testInfo, "Bob's view after deposit is confirmed", trade, true); - break; - } - } + genBtcBlocksThenWait(1, 2_500); - if (!trade.getIsDepositConfirmed()) { - fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, deposit tx was never confirmed.", - trade.getShortId(), - trade.getState(), - trade.getPhase())); - } + waitForDepositConfirmation(log, testInfo, bobClient, trade.getTradeId()); + trade = bobClient.getTrade(tradeId); + verifyTakerDepositConfirmed(trade); + logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId), true); } catch (StatusRuntimeException e) { fail(e); } @@ -140,53 +103,12 @@ public void testBobsConfirmPaymentStarted(final TestInfo testInfo) { try { var trade = bobClient.getTrade(tradeId); - Predicate tradeStateAndPhaseCorrect = (t) -> - t.getState().equals(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN.name()) && t.getPhase().equals(DEPOSIT_CONFIRMED.name()); - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - if (!tradeStateAndPhaseCorrect.test(trade)) { - log.warn("INVALID_PHASE for Bob's trade {} in STATE={} PHASE={}, cannot confirm payment started yet.", - trade.getShortId(), - trade.getState(), - trade.getPhase()); - // fail("Bad trade state and phase."); - sleep(1000 * 10); - trade = bobClient.getTrade(tradeId); - continue; - } else { - break; - } - } - - if (!tradeStateAndPhaseCorrect.test(trade)) { - fail(format("INVALID_PHASE for Bob's trade %s in STATE=%s PHASE=%s, could not confirm payment started.", - trade.getShortId(), - trade.getState(), - trade.getPhase())); - } + verifyTakerDepositConfirmed(trade); bobClient.confirmPaymentStarted(tradeId); - sleep(6000); - - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - trade = bobClient.getTrade(tradeId); + sleep(6_000); - if (!trade.getIsFiatSent()) { - log.warn("Bob still waiting for trade {} BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG, attempt # {}", - trade.getShortId(), - i); - sleep(5000); - continue; - } else { - // Note: offer.state == available - assertEquals(AVAILABLE.name(), trade.getOffer().getState()); - EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG) - .setPhase(FIAT_SENT) - .setFiatSent(true); - verifyExpectedProtocolStatus(trade); - logTrade(log, testInfo, "Bob's view after confirming fiat payment sent", trade); - break; - } - } + waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId); } catch (StatusRuntimeException e) { fail(e); } @@ -196,35 +118,11 @@ public void testBobsConfirmPaymentStarted(final TestInfo testInfo) { @Order(3) public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) { try { - var trade = aliceClient.getTrade(tradeId); - - Predicate tradeStateAndPhaseCorrect = (t) -> - t.getState().equals(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG.name()) - && (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(FIAT_SENT.name())); - for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { - if (!tradeStateAndPhaseCorrect.test(trade)) { - log.warn("INVALID_PHASE for Alice's trade {} in STATE={} PHASE={}, cannot confirm payment received yet.", - trade.getShortId(), - trade.getState(), - trade.getPhase()); - // fail("Bad trade state and phase."); - sleep(1000 * 10); - trade = aliceClient.getTrade(tradeId); - continue; - } else { - break; - } - } - - if (!tradeStateAndPhaseCorrect.test(trade)) { - fail(format("INVALID_PHASE for Alice's trade %s in STATE=%s PHASE=%s, could not confirm payment received.", - trade.getShortId(), - trade.getState(), - trade.getPhase())); - } + waitForSellerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId); + var trade = aliceClient.getTrade(tradeId); aliceClient.confirmPaymentReceived(trade.getTradeId()); - sleep(3000); + sleep(3_000); trade = aliceClient.getTrade(tradeId); assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); @@ -243,7 +141,7 @@ public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) { @Order(4) public void testBobsBtcWithdrawalToExternalAddress(final TestInfo testInfo) { try { - genBtcBlocksThenWait(1, 1000); + genBtcBlocksThenWait(1, 1_000); var trade = bobClient.getTrade(tradeId); logTrade(log, testInfo, "Bob's view before withdrawing funds to external wallet", trade); @@ -251,26 +149,16 @@ public void testBobsBtcWithdrawalToExternalAddress(final TestInfo testInfo) { String toAddress = bitcoinCli.getNewBtcAddress(); bobClient.withdrawFunds(tradeId, toAddress, WITHDRAWAL_TX_MEMO); - genBtcBlocksThenWait(1, 1000); + genBtcBlocksThenWait(1, 1_000); 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)); + logBalances(log, testInfo); } 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 00000000000..ca6e508dec7 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellXMROfferTest.java @@ -0,0 +1,188 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +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.OfferPayload.Direction.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, + getDefaultBuyerSecurityDepositAsPercent(), + alicesXmrAcct.getId(), + TRADE_FEE_CURRENCY_CODE, + NO_TRIGGER_PRICE); + log.info("ALICE'S SELL XMR (BUY BTC) OFFER:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); + genBtcBlocksThenWait(1, 4000); + var offerId = alicesOffer.getId(); + assertTrue(alicesOffer.getIsCurrencyForMakerFeeBtc()); + + var alicesXmrOffers = aliceClient.getMyCryptoCurrencyOffers(btcTradeDirection, XMR); + assertEquals(1, alicesXmrOffers.size()); + + var trade = takeAlicesOffer(offerId, bobsXmrAcct.getId(), TRADE_FEE_CURRENCY_CODE); + + alicesXmrOffers = aliceClient.getMyCryptoCurrencyOffersSortedByDate(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), true); + logTrade(log, testInfo, "Bob's Taker/Buyer View", bobClient.getTrade(tradeId), true); + } 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.info("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), true); + logTrade(log, testInfo, "Bob's Taker/Buyer View (Payment Sent)", bobClient.getTrade(tradeId), true); + } 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 BSQ, Bob would verify payment has been sent to his + // Bisq / BSQ wallet, but we can do no such checks for XMR payments. + // All XMR transfers are done outside Bisq. + log.info("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_PAID. + EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) + .setPhase(PAYOUT_PUBLISHED) + .setPayoutPublished(true) + .setFiatReceived(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's Maker/Seller View (Payment Received)", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Buyer View (Payment Received)", bobClient.getTrade(tradeId), true); + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(4) + public void testAlicesBtcWithdrawalToExternalAddress(final TestInfo testInfo) { + try { + genBtcBlocksThenWait(1, 1_000); + + var trade = aliceClient.getTrade(tradeId); + logTrade(log, testInfo, "Alice's view before withdrawing BTC funds to external wallet", trade); + + String toAddress = bitcoinCli.getNewBtcAddress(); + aliceClient.withdrawFunds(tradeId, toAddress, WITHDRAWAL_TX_MEMO); + + genBtcBlocksThenWait(1, 1_000); + + trade = aliceClient.getTrade(tradeId); + EXPECTED_PROTOCOL_STATUS.setState(WITHDRAW_COMPLETED) + .setPhase(WITHDRAWN) + .setWithdrawn(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's Maker/Seller View (Done)", aliceClient.getTrade(tradeId), true); + logTrade(log, testInfo, "Bob's Taker/Buyer View (Done)", bobClient.getTrade(tradeId), true); + logBalances(log, testInfo); + } catch (StatusRuntimeException e) { + fail(e); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java index e978624c2f0..8ceb3773bf0 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java @@ -24,7 +24,7 @@ import static bisq.apitest.method.wallet.WalletTestUtil.BOBS_INITIAL_BSQ_BALANCES; import static bisq.apitest.method.wallet.WalletTestUtil.bsqBalanceModel; import static bisq.apitest.method.wallet.WalletTestUtil.verifyBsqBalances; -import static bisq.cli.TableFormat.formatBsqBalanceInfoTbl; +import static bisq.cli.table.builder.TableType.BSQ_BALANCE_TBL; import static org.bitcoinj.core.NetworkParameters.ID_REGTEST; import static org.bitcoinj.core.NetworkParameters.PAYMENT_PROTOCOL_ID_REGTEST; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -36,6 +36,7 @@ import bisq.apitest.config.BisqAppConfig; import bisq.apitest.method.MethodTest; import bisq.cli.GrpcClient; +import bisq.cli.table.builder.TableBuilder; @Disabled @Slf4j @@ -73,13 +74,13 @@ public void testInitialBsqBalances(final TestInfo testInfo) { BsqBalanceInfo alicesBsqBalances = aliceClient.getBsqBalances(); log.debug("{} -> Alice's BSQ Initial Balances -> \n{}", testName(testInfo), - formatBsqBalanceInfoTbl(alicesBsqBalances)); + new TableBuilder(BSQ_BALANCE_TBL, alicesBsqBalances).build()); verifyBsqBalances(ALICES_INITIAL_BSQ_BALANCES, alicesBsqBalances); BsqBalanceInfo bobsBsqBalances = bobClient.getBsqBalances(); log.debug("{} -> Bob's BSQ Initial Balances -> \n{}", testName(testInfo), - formatBsqBalanceInfoTbl(bobsBsqBalances)); + new TableBuilder(BSQ_BALANCE_TBL, bobsBsqBalances).build()); verifyBsqBalances(BOBS_INITIAL_BSQ_BALANCES, bobsBsqBalances); } @@ -100,19 +101,19 @@ public void testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(final TestInfo t alicedaemon); verifyBsqBalances(bsqBalanceModel(150000000, - 2500050, - 0, - 0, - 0, - 0), + 2500050, + 0, + 0, + 0, + 0), bobsBsqBalances); verifyBsqBalances(bsqBalanceModel(97499950, - 97499950, - 97499950, - 0, - 0, - 0), + 97499950, + 97499950, + 0, + 0, + 0), alicesBsqBalances); } @@ -133,19 +134,19 @@ public void testBalancesAfterSendingBsqAndGeneratingBtcBlock(final TestInfo test alicedaemon); verifyBsqBalances(bsqBalanceModel(152500050, - 0, - 0, - 0, - 0, - 0), + 0, + 0, + 0, + 0, + 0), bobsBsqBalances); verifyBsqBalances(bsqBalanceModel(97499950, - 0, - 0, - 0, - 0, - 0), + 0, + 0, + 0, + 0, + 0), alicesBsqBalances); } @@ -187,12 +188,12 @@ private void printBobAndAliceBsqBalances(final TestInfo testInfo, testName(testInfo), senderApp.equals(bobdaemon) ? "Sending" : "Receiving", SEND_BSQ_AMOUNT, - formatBsqBalanceInfoTbl(bobsBsqBalances)); + new TableBuilder(BSQ_BALANCE_TBL, bobsBsqBalances).build()); log.debug("{} -> Alice's Balances After {} {} BSQ-> \n{}", testName(testInfo), senderApp.equals(alicedaemon) ? "Sending" : "Receiving", SEND_BSQ_AMOUNT, - formatBsqBalanceInfoTbl(alicesBsqBalances)); + new TableBuilder(BSQ_BALANCE_TBL, alicesBsqBalances).build()); } } 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 19d065c6cca..550695e353f 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.BisqAppConfig.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 bisq.apitest.method.MethodTest; +import bisq.cli.table.builder.TableBuilder; @Disabled @Slf4j @@ -54,10 +54,14 @@ public void testInitialBtcBalances(final TestInfo testInfo) { // 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 void testFundAlicesBtcWallet(final TestInfo testInfo) { 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 void testFundAlicesBtcWallet(final TestInfo testInfo) { 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 void testAliceSendBTCToBob(TestInfo testInfo) { 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 void testAliceSendBTCToBob(TestInfo testInfo) { 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/scenario/LongRunningOfferDeactivationTest.java b/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java index e7f09247a86..8cff63fdd5c 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java @@ -59,7 +59,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest { public void testSellOfferAutoDisable(final TestInfo testInfo) { PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, "US"); double mktPriceAsDouble = aliceClient.getBtcPrice("USD"); - long triggerPrice = calcPriceAsLong.apply(mktPriceAsDouble, -50.0000); + long triggerPrice = calcFiatTriggerPriceAsLong.apply(mktPriceAsDouble, -50.0000); log.info("Current USD mkt price = {} Trigger Price = {}", mktPriceAsDouble, formatPrice(triggerPrice)); OfferInfo offer = aliceClient.createMarketBasedPricedOffer(SELL.name(), "USD", @@ -107,7 +107,7 @@ public void testSellOfferAutoDisable(final TestInfo testInfo) { public void testBuyOfferAutoDisable(final TestInfo testInfo) { PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, "US"); double mktPriceAsDouble = aliceClient.getBtcPrice("USD"); - long triggerPrice = calcPriceAsLong.apply(mktPriceAsDouble, 50.0000); + long triggerPrice = calcFiatTriggerPriceAsLong.apply(mktPriceAsDouble, 50.0000); log.info("Current USD mkt price = {} Trigger Price = {}", mktPriceAsDouble, formatPrice(triggerPrice)); OfferInfo offer = aliceClient.createMarketBasedPricedOffer(BUY.name(), "USD", diff --git a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java index 41ac197f1b5..a8eb14c1b05 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java @@ -32,6 +32,7 @@ import bisq.apitest.method.offer.CreateBSQOffersTest; import bisq.apitest.method.offer.CreateOfferUsingFixedPriceTest; import bisq.apitest.method.offer.CreateOfferUsingMarketPriceMarginTest; +import bisq.apitest.method.offer.CreateXMROffersTest; import bisq.apitest.method.offer.EditOfferTest; import bisq.apitest.method.offer.ValidateCreateOfferTest; @@ -90,6 +91,19 @@ public void testCreateBSQOffers() { @Test @Order(6) + public void testCreateXMROffers() { + CreateXMROffersTest test = new CreateXMROffersTest(); + CreateXMROffersTest.createXmrPaymentAccounts(); + test.testCreateFixedPriceBuy1BTCFor200KXMROffer(); + test.testCreateFixedPriceSell1BTCFor200KXMROffer(); + test.testCreatePriceMarginBasedBuy1BTCOfferWithTriggerPrice(); + test.testCreatePriceMarginBasedSell1BTCOffer(); + test.testGetAllMyXMROffers(); + test.testGetAvailableXMROffers(); + } + + @Test + @Order(7) public void testEditOffer() { EditOfferTest test = new EditOfferTest(); // Edit fiat offer tests @@ -112,5 +126,12 @@ public void testEditOffer() { test.testEditFixedPriceOnBsqOffer(); test.testDisableBsqOffer(); test.testEditFixedPriceAndDisableBsqOffer(); + // Edit xmr offer tests + test.testChangePriceMarginBasedXmrOfferWithTriggerPriceToFixedPricedAndDeactivateIt(); + test.testChangeFixedPricedXmrOfferToPriceMarginBasedOfferWithTriggerPrice(); + test.testEditTriggerPriceOnFixedPriceXmrOfferShouldThrowException(); + test.testEditFixedPriceOnXmrOffer(); + test.testDisableXmrOffer(); + test.testEditFixedPriceAndDisableXmrOffer(); } } diff --git a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java index 8d03b7dd470..027d216f5fe 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java @@ -83,6 +83,13 @@ public void testCreatePaymentAccount(TestInfo testInfo) { test.testCreateUSPostalMoneyOrderAccount(testInfo); test.testCreateWeChatPayAccount(testInfo); test.testCreateWesternUnionAccount(testInfo); + + test.testCreateBSQAccount(testInfo); + test.testCreateBSQAccountWithInvalidAddressShouldThrowException(testInfo); + test.testCreateInstantBSQAccount(testInfo); + test.testCreateXMRAccount(testInfo); + test.testCreateInstantXMRAccount(testInfo); + test.testCreateInstantXMRAccountWithInvalidAddressShouldThrowException(testInfo); } @AfterAll diff --git a/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java index 1cd4054d61d..9677688166a 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java @@ -32,8 +32,10 @@ import bisq.apitest.method.trade.TakeBuyBSQOfferTest; import bisq.apitest.method.trade.TakeBuyBTCOfferTest; import bisq.apitest.method.trade.TakeBuyBTCOfferWithNationalBankAcctTest; +import bisq.apitest.method.trade.TakeBuyXMROfferTest; import bisq.apitest.method.trade.TakeSellBSQOfferTest; import bisq.apitest.method.trade.TakeSellBTCOfferTest; +import bisq.apitest.method.trade.TakeSellXMROfferTest; @Slf4j @@ -67,17 +69,6 @@ public void testTakeSellBTCOffer(final TestInfo testInfo) { @Test @Order(3) - public void testTakeBuyBSQOffer(final TestInfo testInfo) { - TakeBuyBSQOfferTest test = new TakeBuyBSQOfferTest(); - TakeBuyBSQOfferTest.createBsqPaymentAccounts(); - test.testTakeAlicesSellBTCForBSQOffer(testInfo); - test.testBobsConfirmPaymentStarted(testInfo); - test.testAlicesConfirmPaymentReceived(testInfo); - test.testBobsKeepFunds(testInfo); - } - - @Test - @Order(4) public void testTakeBuyBTCOfferWithNationalBankAcct(final TestInfo testInfo) { TakeBuyBTCOfferWithNationalBankAcctTest test = new TakeBuyBTCOfferWithNationalBankAcctTest(); test.testTakeAlicesBuyOffer(testInfo); @@ -87,6 +78,17 @@ public void testTakeBuyBTCOfferWithNationalBankAcct(final TestInfo testInfo) { test.testAlicesKeepFunds(testInfo); } + @Test + @Order(4) + public void testTakeBuyBSQOffer(final TestInfo testInfo) { + TakeBuyBSQOfferTest test = new TakeBuyBSQOfferTest(); + TakeBuyBSQOfferTest.createBsqPaymentAccounts(); + test.testTakeAlicesSellBTCForBSQOffer(testInfo); + test.testBobsConfirmPaymentStarted(testInfo); + test.testAlicesConfirmPaymentReceived(testInfo); + test.testBobsKeepFunds(testInfo); + } + @Test @Order(5) public void testTakeSellBSQOffer(final TestInfo testInfo) { @@ -97,4 +99,28 @@ public void testTakeSellBSQOffer(final TestInfo testInfo) { test.testBobsConfirmPaymentReceived(testInfo); test.testAlicesBtcWithdrawalToExternalAddress(testInfo); } + + @Test + @Order(6) + public void testTakeBuyXMROffer(final TestInfo testInfo) { + TakeBuyXMROfferTest test = new TakeBuyXMROfferTest(); + TakeBuyXMROfferTest.createXmrPaymentAccounts(); + TakeBuyXMROfferTest.createBsqPaymentAccounts(); + test.testTakeAlicesSellBTCForXMROffer(testInfo); + test.testBobsConfirmPaymentStarted(testInfo); + test.testAlicesConfirmPaymentReceived(testInfo); + test.testBobsKeepFunds(testInfo); + } + + @Test + @Order(7) + public void testTakeSellXMROffer(final TestInfo testInfo) { + TakeSellXMROfferTest test = new TakeSellXMROfferTest(); + TakeBuyXMROfferTest.createXmrPaymentAccounts(); + TakeSellXMROfferTest.createBsqPaymentAccounts(); + test.testTakeAlicesBuyBTCForXMROffer(testInfo); + test.testAlicesConfirmPaymentStarted(testInfo); + test.testBobsConfirmPaymentReceived(testInfo); + test.testAlicesBtcWithdrawalToExternalAddress(testInfo); + } } 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 d81f385a2ba..97ed3dde1a7 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/RobotBob.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/RobotBob.java @@ -20,9 +20,9 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import static bisq.apitest.method.MethodTest.formatBalancesTbls; import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE; import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.isShutdownCalled; -import static bisq.cli.TableFormat.formatBalancesTbls; import static java.util.concurrent.TimeUnit.SECONDS; 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 51d59e7537d..6ba1729f557 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 static bisq.apitest.scenario.bot.protocol.ProtocolStep.*; import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled; +import static bisq.cli.table.builder.TableType.TRADE_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.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 { @@ -133,7 +134,8 @@ protected void printBotProtocolStep() { try { var t = this.getBotClient().getTrade(trade.getTradeId()); if (t.getIsFiatSent()) { - log.info("Buyer has started payment for trade:\n{}", TradeFormat.format(t)); + log.info("Buyer has started payment for trade:\n{}", + new TableBuilder(TRADE_TBL, t).build()); return t; } } catch (Exception ex) { @@ -167,7 +169,8 @@ protected void printBotProtocolStep() { try { var t = this.getBotClient().getTrade(trade.getTradeId()); if (t.getIsFiatReceived()) { - log.info("Seller has received payment for trade:\n{}", TradeFormat.format(t)); + log.info("Seller has received payment for trade:\n{}", + new TableBuilder(TRADE_TBL, t).build()); return t; } } catch (Exception ex) { @@ -202,7 +205,7 @@ protected void printBotProtocolStep() { if (t.getIsPayoutPublished()) { log.info("Payout tx {} has been published for trade:\n{}", t.getPayoutTxId(), - TradeFormat.format(t)); + new TableBuilder(TRADE_TBL, t).build()); return t; } } catch (Exception ex) { 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 0ce26002ece..a3ffba517cd 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 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_TBL; @@ -26,7 +26,7 @@ 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 { @@ -65,7 +65,7 @@ public void run() { 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 +98,8 @@ public void run() { 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_TBL, trade).build()); 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/TakerBotProtocol.java b/apitest/src/test/java/bisq/apitest/scenario/bot/protocol/TakerBotProtocol.java index 63b700824f6..99f3b994229 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.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.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 { @@ -64,7 +65,7 @@ public void run() { 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); diff --git a/build.gradle b/build.gradle index eac410deac4..7796b452360 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ configure(subprojects) { bcVersion = '1.63' bitcoinjVersion = '3186b20' codecVersion = '1.13' + cowwocVersion = '1.2' easybindVersion = '1.0.3' easyVersion = '4.0.1' findbugsVersion = '3.0.2' @@ -395,6 +396,7 @@ configure(project(':cli')) { testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" testCompileOnly "org.projectlombok:lombok:$lombokVersion" testRuntime "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" + testImplementation "org.bitbucket.cowwoc:diff-match-patch:$cowwocVersion" } test { diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 7a52f9a4cc8..d69fbf5549b 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -34,6 +34,8 @@ import java.io.PrintStream; import java.io.PrintWriter; +import java.math.BigDecimal; + import java.util.Date; import java.util.List; @@ -41,13 +43,13 @@ import static bisq.cli.CurrencyFormat.*; 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 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; +import static java.math.BigDecimal.ZERO; @@ -76,6 +78,7 @@ import bisq.cli.opts.UnlockWalletOptionParser; import bisq.cli.opts.VerifyBsqSentToAddressOptionParser; import bisq.cli.opts.WithdrawFundsOptionParser; +import bisq.cli.table.builder.TableBuilder; /** * A command-line client for the Bisq gRPC API. @@ -167,15 +170,19 @@ public static void run(String[] args) { var balances = client.getBalances(currencyCode); switch (currencyCode.toUpperCase()) { case "BSQ": - out.println(formatBsqBalanceInfoTbl(balances.getBsq())); + new TableBuilder(BSQ_BALANCE_TBL, balances.getBsq()).build().print(out); break; case "BTC": - out.println(formatBtcBalanceInfoTbl(balances.getBtc())); + new TableBuilder(BTC_BALANCE_TBL, balances.getBtc()).build().print(out); break; case "": - default: - out.println(formatBalancesTbls(balances)); + default: { + out.println("BTC"); + new TableBuilder(BTC_BALANCE_TBL, balances.getBtc()).build().print(out); + out.println("BSQ"); + new TableBuilder(BSQ_BALANCE_TBL, balances.getBsq()).build().print(out); break; + } } return; } @@ -187,7 +194,7 @@ public static void run(String[] args) { } 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: { @@ -207,7 +214,7 @@ public static void run(String[] args) { return; } var fundingAddresses = client.getFundingAddresses(); - out.println(formatAddressBalanceTbl(fundingAddresses)); + new TableBuilder(ADDRESS_BALANCE_TBL, fundingAddresses).build().print(out); return; } case getunusedbsqaddress: { @@ -316,7 +323,7 @@ public static void run(String[] args) { } 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: { @@ -347,7 +354,7 @@ public static void run(String[] args) { paymentAcctId, makerFeeCurrencyCode, triggerPrice); - out.println(formatOfferTable(singletonList(offer), currencyCode)); + out.println(new TableBuilder(OFFER_TBL, offer).build()); return; } case editoffer: { @@ -360,7 +367,7 @@ public static void run(String[] args) { var fixedPrice = opts.getFixedPrice(); var isUsingMktPriceMargin = opts.isUsingMktPriceMargin(); var marketPriceMargin = opts.getMktPriceMarginAsBigDecimal(); - var triggerPrice = toInternalFiatPrice(opts.getTriggerPriceAsBigDecimal()); + var triggerPrice = toInternalTriggerPrice(client, offerId, opts.getTriggerPriceAsBigDecimal()); var enable = opts.getEnableAsSignedInt(); var editOfferType = opts.getOfferEditType(); client.editOffer(offerId, @@ -392,7 +399,7 @@ public static void run(String[] args) { } var offerId = opts.getOfferId(); var offer = client.getOffer(offerId); - out.println(formatOfferTable(singletonList(offer), offer.getCounterCurrencyCode())); + out.println(new TableBuilder(OFFER_TBL, offer).build()); return; } case getmyoffer: { @@ -403,7 +410,7 @@ public static void run(String[] args) { } var offerId = opts.getOfferId(); var offer = client.getMyOffer(offerId); - out.println(formatOfferTable(singletonList(offer), offer.getCounterCurrencyCode())); + out.println(new TableBuilder(OFFER_TBL, offer).build()); return; } case getoffers: { @@ -418,7 +425,7 @@ public static void run(String[] args) { if (offers.isEmpty()) out.printf("no %s %s offers found%n", direction, currencyCode); else - out.println(formatOfferTable(offers, currencyCode)); + out.println(new TableBuilder(OFFER_TBL, offers).build()); return; } @@ -434,7 +441,7 @@ public static void run(String[] args) { if (offers.isEmpty()) out.printf("no %s %s offers found%n", direction, currencyCode); else - out.println(formatOfferTable(offers, currencyCode)); + out.println(new TableBuilder(OFFER_TBL, offers).build()); return; } @@ -464,7 +471,7 @@ public static void run(String[] args) { if (showContract) out.println(trade.getContractAsJson()); else - out.println(TradeFormat.format(trade)); + new TableBuilder(TRADE_TBL, trade).build().print(out); return; } @@ -556,11 +563,12 @@ public static void run(String[] args) { } 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; @@ -574,7 +582,7 @@ public static void run(String[] args) { address, isTradeInstant); out.println("payment account saved"); - out.println(formatPaymentAcctTbl(singletonList(paymentAccount))); + new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount).build().print(out); return; } case getpaymentaccts: { @@ -584,7 +592,7 @@ public static void run(String[] args) { } 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"); @@ -708,6 +716,25 @@ private static long toLong(String param) { } } + private static long toInternalTriggerPrice(GrpcClient client, + String offerId, + BigDecimal unscaledTriggerPrice) { + if (unscaledTriggerPrice.compareTo(ZERO) >= 0) { + // Unfortunately, the EditOffer proto triggerPrice field was declared as + // a long instead of a string, so the CLI has to look at the offer to know + // how to scale the trigger-price (for a fiat or altcoin offer) param sent + // to the server in its 'editoffer' request. That means a preliminary round + // trip to the server: a 'getmyoffer' request. + var offer = client.getMyOffer(offerId); + if (offer.getCounterCurrencyCode().equals("BTC")) + return toInternalCryptoCurrencyPrice(unscaledTriggerPrice); + else + return toInternalFiatPrice(unscaledTriggerPrice); + } else { + return 0L; + } + } + private static File saveFileToDisk(String prefix, @SuppressWarnings("SameParameterValue") String suffix, String text) { @@ -821,8 +848,8 @@ private static void printHelp(OptionParser parser, @SuppressWarnings("SameParame 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 cb503d7c079..63ebca9b54c 100644 --- a/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java +++ b/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java @@ -20,15 +20,16 @@ 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("BSQ"); + result.add("XMR"); 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 8d8a3d11fde..a0c6b3ad8dc 100644 --- a/cli/src/main/java/bisq/cli/CurrencyFormat.java +++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java @@ -142,8 +142,12 @@ public static String formatCryptoCurrencyOfferVolume(long volume) { return FRIENDLY_NUMBER_FORMAT.format((double) volume / SATOSHI_DIVISOR.doubleValue()); } - public static long toInternalFiatPrice(BigDecimal humanFriendlyFiatPrice) { - return humanFriendlyFiatPrice.multiply(new BigDecimal(10_000)).longValue(); + public static long toInternalFiatPrice(BigDecimal fiatPrice) { + return fiatPrice.multiply(new BigDecimal(10_000)).longValue(); + } + + public static long toInternalCryptoCurrencyPrice(BigDecimal altcoinPrice) { + return altcoinPrice.multiply(new BigDecimal(100_000_000)).longValue(); } public static long toSatoshis(String btc) { diff --git a/cli/src/main/java/bisq/cli/DirectionFormat.java b/cli/src/main/java/bisq/cli/DirectionFormat.java deleted file mode 100644 index ac0e5b6c556..00000000000 --- a/cli/src/main/java/bisq/cli/DirectionFormat.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.cli; - -import bisq.proto.grpc.OfferInfo; - -import java.util.List; -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; - -class DirectionFormat { - - static int getLongestDirectionColWidth(List offers) { - if (offers.isEmpty() || offers.get(0).getBaseCurrencyCode().equals("BTC")) - return COL_HEADER_DIRECTION.length(); - else - return 18; // .e.g., "Sell BSQ (Buy BTC)".length() - } - - static final Function directionFormat = (offer) -> { - String baseCurrencyCode = offer.getBaseCurrencyCode(); - boolean isCryptoCurrencyOffer = !baseCurrencyCode.equals("BTC"); - if (!isCryptoCurrencyOffer) { - return baseCurrencyCode; - } else { - // Return "Sell BSQ (Buy BTC)", or "Buy BSQ (Sell BTC)". - String direction = offer.getDirection(); - String mirroredDirection = getMirroredDirection(direction); - Function mixedCase = (word) -> word.charAt(0) + word.substring(1).toLowerCase(); - return format("%s %s (%s %s)", - mixedCase.apply(mirroredDirection), - baseCurrencyCode, - mixedCase.apply(direction), - offer.getCounterCurrencyCode()); - } - }; - - static String getMirroredDirection(String directionAsString) { - return directionAsString.equalsIgnoreCase(BUY.name()) ? SELL.name() : BUY.name(); - } -} diff --git a/cli/src/main/java/bisq/cli/GrpcClient.java b/cli/src/main/java/bisq/cli/GrpcClient.java index 9b2195bf20d..8c7e8f99fbd 100644 --- a/cli/src/main/java/bisq/cli/GrpcClient.java +++ b/cli/src/main/java/bisq/cli/GrpcClient.java @@ -267,8 +267,8 @@ public List getOffersSortedByDate(String direction, String currencyCo return offersServiceRequest.getOffersSortedByDate(direction, currencyCode); } - public List getBsqOffersSortedByDate() { - return offersServiceRequest.getBsqOffersSortedByDate(); + public List getCryptoCurrencyOffersSortedByDate(String currencyCode) { + return offersServiceRequest.getCryptoCurrencyOffersSortedByDate(currencyCode); } public List getMyOffers(String direction, String currencyCode) { @@ -283,12 +283,8 @@ public List getMyOffersSortedByDate(String direction, String currency return offersServiceRequest.getMyOffersSortedByDate(direction, currencyCode); } - public List getMyOffersSortedByDate(String currencyCode) { - return offersServiceRequest.getMyOffersSortedByDate(currencyCode); - } - - public List getMyBsqOffersSortedByDate() { - return offersServiceRequest.getMyBsqOffersSortedByDate(); + public List getMyCryptoCurrencyOffersSortedByDate(String currencyCode) { + return offersServiceRequest.getMyCryptoCurrencyOffersSortedByDate(currencyCode); } public OfferInfo getMostRecentOffer(String direction, String currencyCode) { 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 1340ac3a760..00000000000 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ /dev/null @@ -1,379 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.cli; - -import bisq.proto.grpc.AddressBalanceInfo; -import bisq.proto.grpc.BalancesInfo; -import bisq.proto.grpc.BsqBalanceInfo; -import bisq.proto.grpc.BtcBalanceInfo; -import bisq.proto.grpc.OfferInfo; - -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, "BTC"); - - 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 "BTC" + "\n" - + formatBtcBalanceInfoTbl(balancesInfo.getBtc()) + "\n" - + "BSQ" + "\n" - + formatBsqBalanceInfoTbl(balancesInfo.getBsq()); - } - - public static String formatBsqBalanceInfoTbl(BsqBalanceInfo bsqBalanceInfo) { - String headerLine = COL_HEADER_AVAILABLE_CONFIRMED_BALANCE + COL_HEADER_DELIMITER - + COL_HEADER_UNVERIFIED_BALANCE + COL_HEADER_DELIMITER - + COL_HEADER_UNCONFIRMED_CHANGE_BALANCE + COL_HEADER_DELIMITER - + COL_HEADER_LOCKED_FOR_VOTING_BALANCE + COL_HEADER_DELIMITER - + COL_HEADER_LOCKUP_BONDS_BALANCE + COL_HEADER_DELIMITER - + COL_HEADER_UNLOCKING_BONDS_BALANCE + COL_HEADER_DELIMITER + "\n"; - String colDataFormat = "%" + COL_HEADER_AVAILABLE_CONFIRMED_BALANCE.length() + "s" // rt justify - + " %" + (COL_HEADER_UNVERIFIED_BALANCE.length() + 1) + "s" // rt justify - + " %" + (COL_HEADER_UNCONFIRMED_CHANGE_BALANCE.length() + 1) + "s" // rt justify - + " %" + (COL_HEADER_LOCKED_FOR_VOTING_BALANCE.length() + 1) + "s" // rt justify - + " %" + (COL_HEADER_LOCKUP_BONDS_BALANCE.length() + 1) + "s" // rt justify - + " %" + (COL_HEADER_UNLOCKING_BONDS_BALANCE.length() + 1) + "s"; // rt justify - return headerLine + format(colDataFormat, - formatBsq(bsqBalanceInfo.getAvailableConfirmedBalance()), - formatBsq(bsqBalanceInfo.getUnverifiedBalance()), - formatBsq(bsqBalanceInfo.getUnconfirmedChangeBalance()), - formatBsq(bsqBalanceInfo.getLockedForVotingBalance()), - formatBsq(bsqBalanceInfo.getLockupBondsBalance()), - formatBsq(bsqBalanceInfo.getUnlockingBondsBalance())); - } - - 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 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 offer list is empty", currencyCode.toLowerCase())); - - String baseCurrencyCode = offers.get(0).getBaseCurrencyCode(); - boolean isMyOffer = offers.get(0).getIsMyOffer(); - return baseCurrencyCode.equalsIgnoreCase("BTC") - ? formatFiatOfferTable(offers, currencyCode, isMyOffer) - : formatCryptoCurrencyOfferTable(offers, baseCurrencyCode, isMyOffer); - } - - private static String formatFiatOfferTable(List offers, - String fiatCurrencyCode, - boolean isMyOffer) { - // 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); - // "Enabled" and "Trigger Price" columns are displayed for my offers only. - String enabledHeaderFormat = isMyOffer ? - COL_HEADER_ENABLED + COL_HEADER_DELIMITER - : ""; - String triggerPriceHeaderFormat = isMyOffer ? - // COL_HEADER_TRIGGER_PRICE includes %s -> fiatCurrencyCode - COL_HEADER_TRIGGER_PRICE + COL_HEADER_DELIMITER - : ""; - String headersFormat = enabledHeaderFormat - + COL_HEADER_DIRECTION + COL_HEADER_DELIMITER - // COL_HEADER_PRICE includes %s -> fiatCurrencyCode - + COL_HEADER_PRICE + COL_HEADER_DELIMITER - + padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER - // COL_HEADER_VOLUME includes %s -> fiatCurrencyCode - + padStart(COL_HEADER_VOLUME, volumeColWidth, ' ') + COL_HEADER_DELIMITER - + triggerPriceHeaderFormat - + 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(), - // COL_HEADER_TRIGGER_PRICE includes %s -> fiatCurrencyCode - isMyOffer ? fiatCurrencyCode.toUpperCase() : ""); - String colDataFormat = getFiatOfferColDataFormat(isMyOffer, - amountColWith, - volumeColWidth, - paymentMethodColWidth); - return formattedFiatOfferTable(offers, isMyOffer, headerLine, colDataFormat); - } - - private static String formattedFiatOfferTable(List offers, - boolean isMyOffer, - String headerLine, - String colDataFormat) { - if (isMyOffer) { - return headerLine - + offers.stream() - .map(o -> format(colDataFormat, - formatEnabled(o), - o.getDirection(), - formatPrice(o.getPrice()), - formatAmountRange(o.getMinAmount(), o.getAmount()), - formatVolumeRange(o.getMinVolume(), o.getVolume()), - o.getTriggerPrice() == 0 ? "" : formatPrice(o.getTriggerPrice()), - o.getPaymentMethodShortName(), - formatTimestamp(o.getDate()), - o.getId())) - .collect(Collectors.joining("\n")); - } else { - 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 getFiatOfferColDataFormat(boolean isMyOffer, - int amountColWith, - int volumeColWidth, - int paymentMethodColWidth) { - if (isMyOffer) { - return "%-" + (COL_HEADER_ENABLED.length() + COL_HEADER_DELIMITER.length()) + "s" - + "%-" + (COL_HEADER_DIRECTION.length() + COL_HEADER_DELIMITER.length()) + "s" - + "%" + (COL_HEADER_PRICE.length() - 1) + "s" - + " %" + amountColWith + "s" - + " %" + (volumeColWidth - 1) + "s" - + " %" + (COL_HEADER_TRIGGER_PRICE.length() - 1) + "s" - + " %-" + paymentMethodColWidth + "s" - + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" - + " %-" + COL_HEADER_UUID.length() + "s"; - } else { - return "%-" + (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"; - } - } - - private static String formatCryptoCurrencyOfferTable(List offers, - String cryptoCurrencyCode, - boolean isMyOffer) { - // 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); - // "Enabled" column is displayed for my offers only. - String enabledHeaderFormat = isMyOffer ? - COL_HEADER_ENABLED + COL_HEADER_DELIMITER - : ""; - // TODO use memoize function to avoid duplicate the formatting done above? - String headersFormat = enabledHeaderFormat - + 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; - if (isMyOffer) { - colDataFormat = "%-" + (COL_HEADER_ENABLED.length() + COL_HEADER_DELIMITER.length()) + "s" - + "%-" + 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"; - } else { - 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"; - } - if (isMyOffer) { - return headerLine - + offers.stream() - .map(o -> format(colDataFormat, - formatEnabled(o), - 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")); - } else { - 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 String formatEnabled(OfferInfo offerInfo) { - if (offerInfo.getIsMyOffer() && offerInfo.getIsMyPendingOffer()) - return "PENDING"; - else - return offerInfo.getIsActivated() ? "YES" : "NO"; - } - - 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 57248f4da02..00000000000 --- a/cli/src/main/java/bisq/cli/TradeFormat.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.cli; - -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" - : ""; - - boolean showBsqBuyerAddress = shouldShowBsqBuyerAddress(tradeInfo, isTaker); - Supplier bsqBuyerAddressHeader = () -> showBsqBuyerAddress ? COL_HEADER_TRADE_BSQ_BUYER_ADDRESS : ""; - Supplier bsqBuyerAddressHeaderSpec = () -> showBsqBuyerAddress ? "%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 - + bsqBuyerAddressHeader.get() - + "%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" - + bsqBuyerAddressHeaderSpec.get(); - - return headerLine + formatTradeData(colDataFormat, tradeInfo, isTaker, showBsqBuyerAddress); - } - - private static String formatTradeData(String format, - TradeInfo tradeInfo, - boolean isTaker, - boolean showBsqBuyerAddress) { - 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.getIsDepositConfirmed() ? YES : NO, - tradeCostFormat.apply(tradeInfo), - tradeInfo.getIsFiatSent() ? YES : NO, - tradeInfo.getIsFiatReceived() ? YES : NO, - tradeInfo.getIsPayoutPublished() ? YES : NO, - tradeInfo.getIsWithdrawn() ? YES : NO, - bsqReceiveAddress.apply(tradeInfo, showBsqBuyerAddress)); - } - - private static final Function priceHeader = (t) -> - t.getOffer().getBaseCurrencyCode().equals("BTC") - ? COL_HEADER_PRICE - : COL_HEADER_PRICE_OF_ALTCOIN; - - private static final Function priceHeaderCurrencyCode = (t) -> - t.getOffer().getBaseCurrencyCode().equals("BTC") - ? t.getOffer().getCounterCurrencyCode() - : t.getOffer().getBaseCurrencyCode(); - - private static final BiFunction makerTakerFeeHeaderCurrencyCode = (t, isTaker) -> { - if (isTaker) { - return t.getIsCurrencyForTakerFeeBtc() ? "BTC" : "BSQ"; - } else { - return t.getOffer().getIsCurrencyForMakerFeeBtc() ? "BTC" : "BSQ"; - } - }; - - private static final Function paymentStatusHeaderCurrencyCode = (t) -> - t.getOffer().getBaseCurrencyCode().equals("BTC") - ? t.getOffer().getCounterCurrencyCode() - : t.getOffer().getBaseCurrencyCode(); - - private static final Function priceFormat = (t) -> - t.getOffer().getBaseCurrencyCode().equals("BTC") - ? formatPrice(t.getTradePrice()) - : formatCryptoCurrencyPrice(t.getTradePrice()); - - private static final Function amountFormat = (t) -> - t.getOffer().getBaseCurrencyCode().equals("BTC") - ? formatSatoshis(t.getTradeAmountAsLong()) - : formatCryptoCurrencyOfferVolume(t.getTradeVolume()); - - 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) -> { - if (isTaker) { - return t.getIsCurrencyForTakerFeeBtc() - ? formatSatoshis(t.getTakerFeeAsLong()) - : formatBsq(t.getTakerFeeAsLong()); - } else { - return t.getOffer().getIsCurrencyForMakerFeeBtc() - ? formatSatoshis(t.getOffer().getMakerFee()) - : formatBsq(t.getOffer().getMakerFee()); - } - }; - - private static final Function tradeCostFormat = (t) -> - t.getOffer().getBaseCurrencyCode().equals("BTC") - ? formatOfferVolume(t.getTradeVolume()) - : formatSatoshis(t.getTradeAmountAsLong()); - - private static final BiFunction bsqReceiveAddress = (t, showBsqBuyerAddress) -> { - if (showBsqBuyerAddress) { - ContractInfo contract = t.getContract(); - boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker(); - return isBuyerMakerAndSellerTaker // (is BTC buyer / maker) - ? contract.getTakerPaymentAccountPayload().getAddress() - : contract.getMakerPaymentAccountPayload().getAddress(); - } else { - return ""; - } - }; - - private static boolean shouldShowBsqBuyerAddress(TradeInfo tradeInfo, boolean isTaker) { - if (tradeInfo.getOffer().getBaseCurrencyCode().equals("BTC")) { - return false; - } else { - ContractInfo contract = tradeInfo.getContract(); - // Do not forget buyer and seller refer to BTC buyer and seller, not BSQ - // buyer and seller. If you are buying BSQ, you are the (BTC) seller. - boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker(); - if (isTaker) { - return !isBuyerMakerAndSellerTaker; - } else { - return isBuyerMakerAndSellerTaker; - } - } - } -} diff --git a/cli/src/main/java/bisq/cli/TransactionFormat.java b/cli/src/main/java/bisq/cli/TransactionFormat.java deleted file mode 100644 index 608c2fcb71f..00000000000 --- a/cli/src/main/java/bisq/cli/TransactionFormat.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.cli; - -import bisq.proto.grpc.TxInfo; - -import com.google.common.annotations.VisibleForTesting; - -import static bisq.cli.ColumnHeaderConstants.*; -import static bisq.cli.CurrencyFormat.formatSatoshis; -import static com.google.common.base.Strings.padEnd; - -@VisibleForTesting -public class TransactionFormat { - - public static String format(TxInfo txInfo) { - String headerLine = padEnd(COL_HEADER_TX_ID, txInfo.getTxId().length(), ' ') + COL_HEADER_DELIMITER - + COL_HEADER_TX_IS_CONFIRMED + COL_HEADER_DELIMITER - + COL_HEADER_TX_INPUT_SUM + COL_HEADER_DELIMITER - + COL_HEADER_TX_OUTPUT_SUM + COL_HEADER_DELIMITER - + COL_HEADER_TX_FEE + COL_HEADER_DELIMITER - + COL_HEADER_TX_SIZE + COL_HEADER_DELIMITER - + (txInfo.getMemo().isEmpty() ? "" : COL_HEADER_TX_MEMO + COL_HEADER_DELIMITER) - + "\n"; - - String colDataFormat = "%-" + txInfo.getTxId().length() + "s" - + " %" + COL_HEADER_TX_IS_CONFIRMED.length() + "s" - + " %" + COL_HEADER_TX_INPUT_SUM.length() + "s" - + " %" + COL_HEADER_TX_OUTPUT_SUM.length() + "s" - + " %" + COL_HEADER_TX_FEE.length() + "s" - + " %" + COL_HEADER_TX_SIZE.length() + "s" - + " %s"; - - return headerLine - + String.format(colDataFormat, - txInfo.getTxId(), - txInfo.getIsPending() ? "NO" : "YES", // pending=true means not confirmed - formatSatoshis(txInfo.getInputSum()), - formatSatoshis(txInfo.getOutputSum()), - formatSatoshis(txInfo.getFee()), - txInfo.getSize(), - txInfo.getMemo().isEmpty() ? "" : txInfo.getMemo()); - } -} diff --git a/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java b/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java index a37a9f109bb..a3f3b1c9356 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 @@ 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 (bsq only)") + final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "crypto currency code (bsq|xmr)") .withRequiredArg(); - final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "bsq address") + final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "altcoin address") .withRequiredArg(); final OptionSpec tradeInstantOpt = parser.accepts(OPT_TRADE_INSTANT, "create trade instant account") @@ -58,11 +60,14 @@ public CreateCryptoCurrencyPaymentAcctOptionParser parse() { if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty()) throw new IllegalArgumentException("no currency code specified"); - if (!options.valueOf(currencyCodeOpt).equalsIgnoreCase("bsq")) - throw new IllegalArgumentException("api only supports bsq crypto currency payment accounts"); + 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("no bsq address specified"); + 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 42cf8ad1550..5143bbb3dab 100644 --- a/cli/src/main/java/bisq/cli/opts/CreateOfferOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/CreateOfferOptionParser.java @@ -28,14 +28,14 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT, - "id of payment account used for offer") + "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 (bsq|xmr|eur|usd|...)") .withRequiredArg(); final OptionSpec amountOpt = parser.accepts(OPT_AMOUNT, "amount of btc to buy or sell") diff --git a/cli/src/main/java/bisq/cli/opts/GetOffersOptionParser.java b/cli/src/main/java/bisq/cli/opts/GetOffersOptionParser.java index f8a4dee839f..5f7d59eb8fc 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 (bsq|xmr|eur|usd|...)") .withRequiredArg(); public GetOffersOptionParser(String[] args) { diff --git a/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java b/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java index 215c4f3e80d..7a907e5b6c2 100644 --- a/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java +++ b/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java @@ -32,6 +32,7 @@ import java.util.List; import java.util.function.Function; +import static bisq.cli.CryptoCurrencyUtil.apiDoesSupportCryptoCurrency; import static bisq.proto.grpc.EditOfferRequest.EditType.ACTIVATION_STATE_ONLY; import static bisq.proto.grpc.EditOfferRequest.EditType.FIXED_PRICE_ONLY; import static bisq.proto.grpc.EditOfferRequest.EditType.MKT_PRICE_MARGIN_ONLY; @@ -222,7 +223,7 @@ public OfferInfo getMyOffer(String offerId) { } public List getOffers(String direction, String currencyCode) { - if (isSupportedCryptoCurrency(currencyCode)) { + if (apiDoesSupportCryptoCurrency(currencyCode)) { return getCryptoCurrencyOffers(direction, currencyCode); } else { var request = GetOffersRequest.newBuilder() @@ -251,15 +252,15 @@ public List getOffersSortedByDate(String direction, String currencyCo return offers.isEmpty() ? offers : sortOffersByDate(offers); } - public List getBsqOffersSortedByDate() { + public List getCryptoCurrencyOffersSortedByDate(String currencyCode) { ArrayList offers = new ArrayList<>(); - offers.addAll(getCryptoCurrencyOffers(BUY.name(), "BSQ")); - offers.addAll(getCryptoCurrencyOffers(SELL.name(), "BSQ")); + offers.addAll(getCryptoCurrencyOffers(BUY.name(), currencyCode)); + offers.addAll(getCryptoCurrencyOffers(SELL.name(), currencyCode)); return sortOffersByDate(offers); } public List getMyOffers(String direction, String currencyCode) { - if (isSupportedCryptoCurrency(currencyCode)) { + if (apiDoesSupportCryptoCurrency(currencyCode)) { return getMyCryptoCurrencyOffers(direction, currencyCode); } else { var request = GetMyOffersRequest.newBuilder() @@ -281,17 +282,10 @@ public List getMyOffersSortedByDate(String direction, String currency return offers.isEmpty() ? offers : sortOffersByDate(offers); } - public List getMyOffersSortedByDate(String currencyCode) { + public List getMyCryptoCurrencyOffersSortedByDate(String currencyCode) { ArrayList offers = new ArrayList<>(); - offers.addAll(getMyOffers(BUY.name(), currencyCode)); - offers.addAll(getMyOffers(SELL.name(), currencyCode)); - return sortOffersByDate(offers); - } - - public List getMyBsqOffersSortedByDate() { - ArrayList offers = new ArrayList<>(); - offers.addAll(getMyCryptoCurrencyOffers(BUY.name(), "BSQ")); - offers.addAll(getMyCryptoCurrencyOffers(SELL.name(), "BSQ")); + offers.addAll(getMyCryptoCurrencyOffers(BUY.name(), currencyCode)); + offers.addAll(getMyCryptoCurrencyOffers(SELL.name(), currencyCode)); return sortOffersByDate(offers); } @@ -305,15 +299,4 @@ public List sortOffersByDate(List offerInfoList) { .sorted(comparing(OfferInfo::getDate)) .collect(toList()); } - - private static boolean isSupportedCryptoCurrency(String currencyCode) { - return getSupportedCryptoCurrencies().contains(currencyCode.toUpperCase()); - } - - private static List getSupportedCryptoCurrencies() { - final List result = new ArrayList<>(); - result.add("BSQ"); - result.sort(String::compareTo); - return result; - } } 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 00000000000..f306a2ca2fa --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/Table.java @@ -0,0 +1,151 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli.table; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import java.util.stream.IntStream; + +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 + * consume protobufs, which do not support 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; it cannot be joined or queried via SQL. + */ +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 paddedWidth = colIndex == columns.length - 1 + ? c.getName().length() + : c.getWidth() + columnDelimiterLength; + printStream.printf("%-" + paddedWidth + "s", c.getName()); + }); + 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)); + // Print a newline only if now the last column of the last row. + if (colIndex == columns.length - 1 && rowIndex < this.rowCount - 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 00000000000..511a3b97afa --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/AbstractTableBuilder.java @@ -0,0 +1,41 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli.table.builder; + +import bisq.proto.grpc.OfferInfo; + +import java.util.List; +import java.util.function.Predicate; + +/** + * Abstract superclass for TableBuilder implementations. + */ +abstract class AbstractTableBuilder { + + protected final TableType tableType; + protected final List protos; + + public AbstractTableBuilder(TableType tableType, List protos) { + this.tableType = tableType; + this.protos = protos; + if (protos.isEmpty()) + throw new IllegalArgumentException("proto list is empty"); + } + + protected final Predicate isFiatOffer = (o) -> o.getBaseCurrencyCode().equals("BTC"); +} 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 00000000000..c36db9d7ceb --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/AddressBalanceTableBuilder.java @@ -0,0 +1,81 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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. + */ +public 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; + + public 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/BsqBalanceTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/BsqBalanceTableBuilder.java new file mode 100644 index 00000000000..8448b96afbe --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/BsqBalanceTableBuilder.java @@ -0,0 +1,78 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli.table.builder; + +import bisq.proto.grpc.BsqBalanceInfo; + +import java.util.List; + +import static bisq.cli.table.builder.TableBuilderConstants.*; +import static bisq.cli.table.builder.TableType.BSQ_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.BsqBalanceInfo} object. + */ +public class BsqBalanceTableBuilder extends AbstractTableBuilder { + + // Default columns not dynamically generated with bsq balance info. + private final Column colAvailableConfirmedBalance; + private final Column colUnverifiedBalance; + private final Column colUnconfirmedChangeBalance; + private final Column colLockedForVotingBalance; + private final Column colLockupBondsBalance; + private final Column colUnlockingBondsBalance; + + public BsqBalanceTableBuilder(List protos) { + super(BSQ_BALANCE_TBL, protos); + this.colAvailableConfirmedBalance = new SatoshiColumn(COL_HEADER_AVAILABLE_CONFIRMED_BALANCE, true); + this.colUnverifiedBalance = new SatoshiColumn(COL_HEADER_UNVERIFIED_BALANCE, true); + this.colUnconfirmedChangeBalance = new SatoshiColumn(COL_HEADER_UNCONFIRMED_CHANGE_BALANCE, true); + this.colLockedForVotingBalance = new SatoshiColumn(COL_HEADER_LOCKED_FOR_VOTING_BALANCE, true); + this.colLockupBondsBalance = new SatoshiColumn(COL_HEADER_LOCKUP_BONDS_BALANCE, true); + this.colUnlockingBondsBalance = new SatoshiColumn(COL_HEADER_UNLOCKING_BONDS_BALANCE, true); + } + + public Table build() { + BsqBalanceInfo balance = (BsqBalanceInfo) protos.get(0); + + // Populate columns with bsq balance info. + + colAvailableConfirmedBalance.addRow(balance.getAvailableConfirmedBalance()); + colUnverifiedBalance.addRow(balance.getUnverifiedBalance()); + colUnconfirmedChangeBalance.addRow(balance.getUnconfirmedChangeBalance()); + colLockedForVotingBalance.addRow(balance.getLockedForVotingBalance()); + colLockupBondsBalance.addRow(balance.getLockupBondsBalance()); + colUnlockingBondsBalance.addRow(balance.getUnlockingBondsBalance()); + + // Define and return the table instance with populated columns. + + return new Table(colAvailableConfirmedBalance.asStringColumn(), + colUnverifiedBalance.asStringColumn(), + colUnconfirmedChangeBalance.asStringColumn(), + colLockedForVotingBalance.asStringColumn(), + colLockupBondsBalance.asStringColumn(), + colUnlockingBondsBalance.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 00000000000..70781b380ff --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/BtcBalanceTableBuilder.java @@ -0,0 +1,73 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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. + */ +public 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; + + public 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/OfferTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/OfferTableBuilder.java new file mode 100644 index 00000000000..c61ccaf3298 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/OfferTableBuilder.java @@ -0,0 +1,280 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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.AltcoinColumn.DISPLAY_MODE.ALTCOIN_OFFER_VOLUME; +import static bisq.cli.table.column.AltcoinColumn.DISPLAY_MODE.ALTCOIN_TRIGGER_PRICE; +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.FiatColumn.DISPLAY_MODE.TRIGGER_PRICE; +import static bisq.cli.table.column.FiatColumn.DISPLAY_MODE.VOLUME; +import static bisq.cli.table.column.ZippedStringColumns.DUPLICATION_MODE.EXCLUDE_DUPLICATES; +import static java.lang.String.format; +import static protobuf.OfferPayload.Direction.BUY; +import static protobuf.OfferPayload.Direction.SELL; + + + +import bisq.cli.table.Table; +import bisq.cli.table.column.AltcoinColumn; +import bisq.cli.table.column.Column; +import bisq.cli.table.column.FiatColumn; +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. + */ +public 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); + + public OfferTableBuilder(List protos) { + super(OFFER_TBL, protos); + } + + 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 FiatColumn(format(COL_HEADER_PRICE, fiatTradeCurrency.get())); + Column colFiatVolume = new FiatColumn(format("Temp Volume (%s)", fiatTradeCurrency.get()), NONE, VOLUME); + Column colMinFiatVolume = new FiatColumn(format("Temp Min Volume (%s)", fiatTradeCurrency.get()), NONE, VOLUME); + @Nullable + Column colTriggerPrice = fiatTriggerPriceColumn.get(); + + // Populate columns with offer info. + + //noinspection SimplifyStreamApiCallChains + offers.stream().forEachOrdered(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()); + colMinFiatVolume.addRow(o.getMinVolume()); + colFiatVolume.addRow(o.getVolume()); + + if (colTriggerPrice != null) + colTriggerPrice.addRow(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, fiatTradeCurrency.get()), + RIGHT, + " - ", + colMinFiatVolume.asStringColumn(), + colFiatVolume.asStringColumn()); + + // Define and return the table instance with populated columns. + + if (isShowingMyOffers.get()) { + return new Table(colEnabled.asStringColumn(), + colDirection, + colFiatPrice.asStringColumn(), + amountRange.asStringColumn(EXCLUDE_DUPLICATES), + volumeRange.asStringColumn(EXCLUDE_DUPLICATES), + colTriggerPrice.asStringColumn(), + colPaymentMethod, + colCreateDate.asStringColumn(), + colOfferId); + } else { + return new Table(colDirection, + colFiatPrice.asStringColumn(), + 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 SatoshiColumn(format(COL_HEADER_PRICE_OF_ALTCOIN, altcoinTradeCurrency.get())); + Column colBtcVolume = new AltcoinColumn(format("Temp Volume (%s)", altcoinTradeCurrency.get()), + NONE, + ALTCOIN_OFFER_VOLUME); + Column colMinBtcVolume = new AltcoinColumn(format("Temp Min Volume (%s)", altcoinTradeCurrency.get()), + NONE, + ALTCOIN_OFFER_VOLUME); + @Nullable + Column colTriggerPrice = altcoinTriggerPriceColumn.get(); + + // Populate columns with offer info. + + //noinspection SimplifyStreamApiCallChains + offers.stream().forEachOrdered(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()); + colBtcVolume.addRow(o.getMinVolume()); + colMinBtcVolume.addRow(o.getVolume()); + + if (colTriggerPrice != null) + colTriggerPrice.addRow(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, altcoinTradeCurrency.get()), + RIGHT, + " - ", + colBtcVolume.asStringColumn(), + colMinBtcVolume.asStringColumn()); + + // Define and return the table instance with populated columns. + + if (isShowingMyOffers.get()) { + if (isShowingBsqOffers.get()) { + return new Table(colEnabled.asStringColumn(), + colDirection, + colBtcPrice.asStringColumn(), + amountRange.asStringColumn(EXCLUDE_DUPLICATES), + volumeRange.asStringColumn(EXCLUDE_DUPLICATES), + colPaymentMethod, + colCreateDate.asStringColumn(), + colOfferId); + } else { + return new Table(colEnabled.asStringColumn(), + colDirection, + colBtcPrice.asStringColumn(), + amountRange.asStringColumn(EXCLUDE_DUPLICATES), + volumeRange.asStringColumn(EXCLUDE_DUPLICATES), + colTriggerPrice.asStringColumn(), + colPaymentMethod, + colCreateDate.asStringColumn(), + colOfferId); + } + } else { + return new Table(colDirection, + colBtcPrice.asStringColumn(), + amountRange.asStringColumn(EXCLUDE_DUPLICATES), + volumeRange.asStringColumn(EXCLUDE_DUPLICATES), + colPaymentMethod, + colCreateDate.asStringColumn(), + colOfferId); + } + } + + 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 FiatColumn(format(COL_HEADER_TRIGGER_PRICE, fiatTradeCurrency.get()), RIGHT, TRIGGER_PRICE) + : null; + @Nullable + private final Supplier altcoinTriggerPriceColumn = () -> + isShowingMyOffers.get() && !isShowingBsqOffers.get() + ? new AltcoinColumn(format(COL_HEADER_TRIGGER_PRICE, altcoinTradeCurrency.get()), RIGHT, ALTCOIN_TRIGGER_PRICE) + : null; + + private final Function toEnabled = (o) -> { + if (o.getIsMyOffer() && o.getIsMyPendingOffer()) + return "PENDING"; + else + 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, + RIGHT, + " - ", + colMinAmount.asStringColumn(), + colAmount.asStringColumn()); + }; +} 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 00000000000..b674d3f88cb --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/PaymentAccountTableBuilder.java @@ -0,0 +1,74 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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. + */ +public 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; + + public 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 00000000000..1e9e2e2d821 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/TableBuilder.java @@ -0,0 +1,64 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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); + } + + public Table build() { + switch (tableType) { + case ADDRESS_BALANCE_TBL: + return new AddressBalanceTableBuilder(protos).build(); + case BSQ_BALANCE_TBL: + return new BsqBalanceTableBuilder(protos).build(); + case BTC_BALANCE_TBL: + return new BtcBalanceTableBuilder(protos).build(); + case OFFER_TBL: + return new OfferTableBuilder(protos).build(); + case PAYMENT_ACCOUNT_TBL: + return new PaymentAccountTableBuilder(protos).build(); + case TRADE_TBL: + return new TradeTableBuilder(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/ColumnHeaderConstants.java b/cli/src/main/java/bisq/cli/table/builder/TableBuilderConstants.java similarity index 65% rename from cli/src/main/java/bisq/cli/ColumnHeaderConstants.java rename to cli/src/main/java/bisq/cli/table/builder/TableBuilderConstants.java index 32a4564d16d..a0c0bb8bff1 100644 --- a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java +++ b/cli/src/main/java/bisq/cli/table/builder/TableBuilderConstants.java @@ -15,21 +15,13 @@ * along with Bisq. If not, see . */ -package bisq.cli; +package bisq.cli.table.builder; -import static com.google.common.base.Strings.padEnd; -import static com.google.common.base.Strings.padStart; - -class ColumnHeaderConstants { - - // For inserting 2 spaces between column headers. - static final String COL_HEADER_DELIMITER = " "; - - // Table column header format specs, right padded with two spaces. In some cases - // such as COL_HEADER_CREATION_DATE, COL_HEADER_VOLUME and COL_HEADER_UUID, the - // expected max data string length is accounted for. In others, column header - // lengths are expected to be greater than any column value length. - static final String COL_HEADER_ADDRESS = padEnd("%-3s Address", 52, ' '); +/** + * Table column name constants. + */ +class TableBuilderConstants { + static final String COL_HEADER_ADDRESS = "%-3s Address"; static final String COL_HEADER_AMOUNT = "BTC(min - max)"; static final String COL_HEADER_AVAILABLE_BALANCE = "Available Balance"; static final String COL_HEADER_AVAILABLE_CONFIRMED_BALANCE = "Available Confirmed Balance"; @@ -43,7 +35,7 @@ class ColumnHeaderConstants { static final String COL_HEADER_UNVERIFIED_BALANCE = "Unverified Balance"; static final String COL_HEADER_CONFIRMATIONS = "Confirmations"; static final String COL_HEADER_IS_USED_ADDRESS = "Is Used"; - static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date (UTC)", 20, ' '); + static final String COL_HEADER_CREATION_DATE = "Creation Date (UTC)"; static final String COL_HEADER_CURRENCY = "Currency"; static final String COL_HEADER_DIRECTION = "Buy/Sell"; static final String COL_HEADER_ENABLED = "Enabled"; @@ -51,20 +43,20 @@ class ColumnHeaderConstants { static final String COL_HEADER_PAYMENT_METHOD = "Payment Method"; static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC"; static final String COL_HEADER_PRICE_OF_ALTCOIN = "Price in BTC for 1 %-3s"; - static final String COL_HEADER_TRADE_AMOUNT = padStart("Amount(%-3s)", 12, ' '); - static final String COL_HEADER_TRADE_BSQ_BUYER_ADDRESS = "BSQ Buyer Address"; - static final String COL_HEADER_TRADE_BUYER_COST = padEnd("Buyer Cost(%-3s)", 15, ' '); + static final String COL_HEADER_TRADE_ALTCOIN_BUYER_ADDRESS = "%-3s Buyer Address"; + static final String COL_HEADER_TRADE_AMOUNT = "Amount(%-3s)"; + 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 = padEnd("%-3s Sent", 8, ' '); - static final String COL_HEADER_TRADE_PAYMENT_RECEIVED = padEnd("%-3s Received", 12, ' '); + 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_ROLE = "My Role"; static final String COL_HEADER_TRADE_SHORT_ID = "ID"; - static final String COL_HEADER_TRADE_TX_FEE = padEnd("Tx Fee(BTC)", 12, ' '); - static final String COL_HEADER_TRADE_MAKER_FEE = padEnd("Maker Fee(%-3s)", 12, ' '); // "Maker Fee(%-3s)"; - static final String COL_HEADER_TRADE_TAKER_FEE = padEnd("Taker Fee(%-3s)", 12, ' '); // "Taker Fee(%-3s)"; + static final String COL_HEADER_TRADE_TX_FEE = "Tx Fee(BTC)"; + 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_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)"; @@ -73,8 +65,6 @@ class ColumnHeaderConstants { 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 = padEnd("%-3s(min - max)", 15, ' '); - - static final String COL_HEADER_UUID = padEnd("ID", 52, ' '); + static final String COL_HEADER_VOLUME = "%-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 00000000000..5432a0a2920 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/TableType.java @@ -0,0 +1,32 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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, + BSQ_BALANCE_TBL, + BTC_BALANCE_TBL, + OFFER_TBL, + PAYMENT_ACCOUNT_TBL, + TRADE_TBL, + TRANSACTION_TBL +} diff --git a/cli/src/main/java/bisq/cli/table/builder/TradeTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/TradeTableBuilder.java new file mode 100644 index 00000000000..f2a53e181e5 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/TradeTableBuilder.java @@ -0,0 +1,250 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli.table.builder; + +import bisq.proto.grpc.ContractInfo; +import bisq.proto.grpc.TradeInfo; + +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; + +import javax.annotation.Nullable; + +import static bisq.cli.table.builder.TableBuilderConstants.*; +import static bisq.cli.table.builder.TableType.TRADE_TBL; +import static bisq.cli.table.column.AltcoinColumn.DISPLAY_MODE.ALTCOIN_OFFER_VOLUME; +import static bisq.cli.table.column.FiatColumn.DISPLAY_MODE.VOLUME; + + + +import bisq.cli.table.Table; +import bisq.cli.table.column.AltcoinColumn; +import bisq.cli.table.column.BooleanColumn; +import bisq.cli.table.column.Column; +import bisq.cli.table.column.FiatColumn; +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.TradeInfo} object. + */ +public class TradeTableBuilder extends AbstractTableBuilder { + + // Default columns not dynamically generated with trade info. + private final Column colShortId; + private final Column colRole; + private final Column colMinerTxFee; + private final Column colIsDepositPublished; + private final Column colIsDepositConfirmed; + private final Column colIsPayoutPublished; + private final Column colIsFundsWithdrawn; + + public TradeTableBuilder(List protos) { + super(TRADE_TBL, protos); + this.colShortId = new StringColumn(COL_HEADER_TRADE_SHORT_ID); + this.colRole = new StringColumn(COL_HEADER_TRADE_ROLE); + this.colMinerTxFee = new AltcoinColumn(COL_HEADER_TRADE_TX_FEE); + this.colIsDepositPublished = new BooleanColumn(COL_HEADER_TRADE_DEPOSIT_PUBLISHED); + this.colIsDepositConfirmed = new BooleanColumn(COL_HEADER_TRADE_DEPOSIT_CONFIRMED); + this.colIsPayoutPublished = new BooleanColumn(COL_HEADER_TRADE_PAYOUT_PUBLISHED); + this.colIsFundsWithdrawn = new BooleanColumn(COL_HEADER_TRADE_WITHDRAWN); + } + + public Table build() { + // TODO Add 'gettrades --currency --direction(?)' api method, & figure out how to + // show multiple trades in the console. For now, a trade tbl is only one row. + + TradeInfo trade = (TradeInfo) protos.get(0); + + // Declare the columns derived from trade info. + + Column colPrice = toPriceColumn.apply(trade); + Column colAmount = toAmountColumn.apply(trade); + Column colBisqTradeFee = toBisqTradeFeeColumn.apply(trade); + Column tradeCostColumn = toTradeCostColumn.apply(trade); + Column colIsPaymentSent = toPaymentSentColumn.apply(trade); + Column colPaymentReceived = toPaymentReceivedColumn.apply(trade); + @SuppressWarnings("ConstantConditions") @Nullable + Column colAltcoinReceiveAddressColumn = toAltcoinReceiveAddressColumn.apply(trade); + + // Populate columns with trade info. + + colShortId.addRow(trade.getShortId()); + colRole.addRow(trade.getRole()); + colPrice.addRow(trade.getTradePrice()); + colAmount.addRow(toAmount.apply(trade)); + colMinerTxFee.addRow(toMinerTxFee.apply(trade)); + colBisqTradeFee.addRow(toMakerTakerFee.apply(trade)); + colIsDepositPublished.addRow(trade.getIsDepositPublished()); + colIsDepositConfirmed.addRow(trade.getIsDepositConfirmed()); + tradeCostColumn.addRow(toTradeCost.apply(trade)); + colIsPaymentSent.addRow(trade.getIsFiatSent()); + colPaymentReceived.addRow(trade.getIsFiatReceived()); + colIsPayoutPublished.addRow(trade.getIsPayoutPublished()); + colIsFundsWithdrawn.addRow(trade.getIsWithdrawn()); + + // Define and return the table instance with populated columns. + + if (colAltcoinReceiveAddressColumn != null) { + colAltcoinReceiveAddressColumn.addRow(toAltcoinReceiveAddress.apply(trade)); + return new Table(colShortId, + colRole, + colPrice.asStringColumn(), + colAmount.asStringColumn(), + colMinerTxFee.asStringColumn(), + colBisqTradeFee.asStringColumn(), + colIsDepositPublished.asStringColumn(), + colIsDepositConfirmed.asStringColumn(), + tradeCostColumn.asStringColumn(), + colIsPaymentSent.asStringColumn(), + colPaymentReceived.asStringColumn(), + colIsPayoutPublished.asStringColumn(), + colIsFundsWithdrawn.asStringColumn(), + colAltcoinReceiveAddressColumn); + } else { + return new Table(colShortId, + colRole, + colPrice.asStringColumn(), + colAmount.asStringColumn(), + colMinerTxFee.asStringColumn(), + colBisqTradeFee.asStringColumn(), + colIsDepositPublished.asStringColumn(), + colIsDepositConfirmed.asStringColumn(), + tradeCostColumn.asStringColumn(), + colIsPaymentSent.asStringColumn(), + colPaymentReceived.asStringColumn(), + colIsPayoutPublished.asStringColumn(), + colIsFundsWithdrawn.asStringColumn()); + } + } + + protected final Predicate isFiatTrade = (t) -> isFiatOffer.test(t.getOffer()); + + private final Predicate isTaker = (t) -> t.getRole().toLowerCase().contains("taker"); + + private final Function paymentCurrencyCode = (t) -> + isFiatTrade.test(t) + ? t.getOffer().getCounterCurrencyCode() + : t.getOffer().getBaseCurrencyCode(); + + private final Function> toPriceColumn = (t) -> { + String colHeader = isFiatTrade.test(t) + ? String.format(COL_HEADER_PRICE, t.getOffer().getCounterCurrencyCode()) + : String.format(COL_HEADER_PRICE_OF_ALTCOIN, t.getOffer().getBaseCurrencyCode()); + return isFiatTrade.test(t) + ? new FiatColumn(colHeader) + : new AltcoinColumn(colHeader); + }; + + private final Function> toAmountColumn = (t) -> { + String headerCurrencyCode = t.getOffer().getBaseCurrencyCode(); + String colHeader = String.format(COL_HEADER_TRADE_AMOUNT, headerCurrencyCode); + return isFiatTrade.test(t) + ? new SatoshiColumn(colHeader) + : new AltcoinColumn(colHeader, ALTCOIN_OFFER_VOLUME); + }; + + private final Function toAmount = (t) -> + isFiatTrade.test(t) + ? t.getTradeAmountAsLong() + : t.getTradeVolume(); + + private final Function toMinerTxFee = (t) -> + isTaker.test(t) + ? t.getTxFeeAsLong() + : t.getOffer().getTxFee(); + + private final Function> toBisqTradeFeeColumn = (t) -> { + String headerCurrencyCode = isTaker.test(t) + ? t.getIsCurrencyForTakerFeeBtc() ? "BTC" : "BSQ" + : t.getOffer().getIsCurrencyForMakerFeeBtc() ? "BTC" : "BSQ"; + String colHeader = isTaker.test(t) + ? String.format(COL_HEADER_TRADE_TAKER_FEE, headerCurrencyCode) + : String.format(COL_HEADER_TRADE_MAKER_FEE, headerCurrencyCode); + boolean isBsqSatoshis = headerCurrencyCode.equals("BSQ"); + return new SatoshiColumn(colHeader, isBsqSatoshis); + }; + + private final Function toMakerTakerFee = (t) -> + isTaker.test(t) + ? t.getTakerFeeAsLong() + : t.getOffer().getMakerFee(); + + private final Function> toTradeCostColumn = (t) -> { + String headerCurrencyCode = t.getOffer().getCounterCurrencyCode(); + String colHeader = String.format(COL_HEADER_TRADE_BUYER_COST, headerCurrencyCode); + return isFiatTrade.test(t) + ? new FiatColumn(colHeader, VOLUME) + : new SatoshiColumn(colHeader); + }; + + private final Function toTradeCost = (t) -> + isFiatTrade.test(t) + ? t.getTradeVolume() + : t.getTradeAmountAsLong(); + + private final Function> toPaymentSentColumn = (t) -> { + String headerCurrencyCode = paymentCurrencyCode.apply(t); + String colHeader = String.format(COL_HEADER_TRADE_PAYMENT_SENT, headerCurrencyCode); + return new BooleanColumn(colHeader); + }; + + private final Function> toPaymentReceivedColumn = (t) -> { + String headerCurrencyCode = paymentCurrencyCode.apply(t); + String colHeader = String.format(COL_HEADER_TRADE_PAYMENT_RECEIVED, headerCurrencyCode); + return new BooleanColumn(colHeader); + }; + + private 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 + private final Function> toAltcoinReceiveAddressColumn = (t) -> { + if (showAltCoinBuyerAddress.test(t)) { + String headerCurrencyCode = paymentCurrencyCode.apply(t); + String colHeader = String.format(COL_HEADER_TRADE_ALTCOIN_BUYER_ADDRESS, headerCurrencyCode); + return new StringColumn(colHeader); + } else { + return null; + } + }; + + private 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/TransactionTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/TransactionTableBuilder.java new file mode 100644 index 00000000000..fa2cdbcd186 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/TransactionTableBuilder.java @@ -0,0 +1,103 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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. + */ +public 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; + + public 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 00000000000..c62f66a8bd2 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/AbstractColumn.java @@ -0,0 +1,77 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli.table.column; + +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; + } + + 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/AltcoinColumn.java b/cli/src/main/java/bisq/cli/table/column/AltcoinColumn.java new file mode 100644 index 00000000000..3fa09064a85 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/AltcoinColumn.java @@ -0,0 +1,102 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli.table.column; + +import java.util.function.BiFunction; +import java.util.stream.IntStream; + +import static bisq.cli.CurrencyFormat.formatCryptoCurrencyOfferVolume; +import static bisq.cli.CurrencyFormat.formatCryptoCurrencyPrice; +import static bisq.cli.table.column.AltcoinColumn.DISPLAY_MODE.ALTCOIN_PRICE; +import static bisq.cli.table.column.Column.JUSTIFICATION.RIGHT; + +/** + * For displaying altcoin values as volume, price, or optional trigger price + * with appropriate precision. + */ +public class AltcoinColumn extends LongColumn { + + public enum DISPLAY_MODE { + ALTCOIN_OFFER_VOLUME, + ALTCOIN_PRICE, + ALTCOIN_TRIGGER_PRICE + } + + private final DISPLAY_MODE displayMode; + + // The default AltcoinColumn JUSTIFICATION is RIGHT. + // The default AltcoinColumn DISPLAY_MODE is ALTCOIN_PRICE. + public AltcoinColumn(String name) { + this(name, RIGHT, ALTCOIN_PRICE); + } + + public AltcoinColumn(String name, DISPLAY_MODE displayMode) { + this(name, RIGHT, displayMode); + } + + public AltcoinColumn(String name, JUSTIFICATION justification) { + this(name, justification, ALTCOIN_PRICE); + } + + public AltcoinColumn(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_OFFER_VOLUME: + return value > 0 ? formatCryptoCurrencyOfferVolume(value) : ""; + case ALTCOIN_PRICE: + case ALTCOIN_TRIGGER_PRICE: + return value > 0 ? formatCryptoCurrencyPrice(value) : ""; + 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 00000000000..75cc17fc82a --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/BooleanColumn.java @@ -0,0 +1,131 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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/Column.java b/cli/src/main/java/bisq/cli/table/column/Column.java new file mode 100644 index 00000000000..787448c74ab --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/Column.java @@ -0,0 +1,116 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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(); + + /** + * 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/FiatColumn.java b/cli/src/main/java/bisq/cli/table/column/FiatColumn.java new file mode 100644 index 00000000000..e4fc38a1668 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/FiatColumn.java @@ -0,0 +1,95 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli.table.column; + +import java.util.stream.IntStream; + +import static bisq.cli.CurrencyFormat.formatOfferVolume; +import static bisq.cli.CurrencyFormat.formatPrice; +import static bisq.cli.table.column.Column.JUSTIFICATION.RIGHT; +import static bisq.cli.table.column.FiatColumn.DISPLAY_MODE.PRICE; +import static bisq.cli.table.column.FiatColumn.DISPLAY_MODE.TRIGGER_PRICE; + +/** + * For displaying fiat values as volume, price, or optional trigger price + * with appropriate precision. + */ +public class FiatColumn extends LongColumn { + + public enum DISPLAY_MODE { + PRICE, + TRIGGER_PRICE, + 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, PRICE); + } + + public FiatColumn(String name, DISPLAY_MODE displayMode) { + this(name, RIGHT, displayMode); + } + + public FiatColumn(String name, JUSTIFICATION justification) { + this(name, justification, PRICE); + } + + 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; + if (displayMode.equals(TRIGGER_PRICE)) + s = value > 0 ? formatPrice(value) : ""; + else + s = displayMode.equals(PRICE) ? formatPrice(value) : formatOfferVolume(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/Iso8601DateTimeColumn.java b/cli/src/main/java/bisq/cli/table/column/Iso8601DateTimeColumn.java new file mode 100644 index 00000000000..0cd4b98ebe6 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/Iso8601DateTimeColumn.java @@ -0,0 +1,64 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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 00000000000..3875f35a893 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/LongColumn.java @@ -0,0 +1,93 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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/NumberColumn.java b/cli/src/main/java/bisq/cli/table/column/NumberColumn.java new file mode 100644 index 00000000000..42f832f616e --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/NumberColumn.java @@ -0,0 +1,32 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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 00000000000..b50d2c4d3c4 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/SatoshiColumn.java @@ -0,0 +1,81 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli.table.column; + +import java.util.stream.IntStream; + +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() { + // 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 this.stringColumn; + } +} 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 00000000000..ca95bd22660 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/StringColumn.java @@ -0,0 +1,88 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli.table.column; + + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import static bisq.cli.table.column.Column.JUSTIFICATION.LEFT; + +/** + * 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; + } +} 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 00000000000..e2e62376083 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/column/ZippedStringColumns.java @@ -0,0 +1,130 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +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/GetOffersSmokeTest.java b/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java deleted file mode 100644 index f613aea358c..00000000000 --- a/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package bisq.cli; - -import static java.lang.System.out; - -/** - Smoke tests for getoffers method. Useful for examining the format of the console output. - - Prerequisites: - - - Run `./bisq-daemon --apiPassword=xyz --appDataDir=$TESTDIR` - - This can be run on mainnet. - */ -public class GetOffersSmokeTest { - - public static void main(String[] args) { - - out.println(">>> getoffers buy usd"); - CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=usd"}); - out.println(">>> getoffers sell usd"); - CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=usd"}); - - out.println(">>> getoffers buy eur"); - CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=eur"}); - out.println(">>> getoffers sell eur"); - CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=eur"}); - - out.println(">>> getoffers buy gbp"); - CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=gbp"}); - out.println(">>> getoffers sell gbp"); - CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=gbp"}); - - out.println(">>> getoffers buy brl"); - CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=brl"}); - out.println(">>> getoffers sell brl"); - CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=brl"}); - } - -} diff --git a/cli/src/test/java/bisq/cli/opts/OptionParsersTest.java b/cli/src/test/java/bisq/cli/opts/OptionParsersTest.java index 58b8712fe90..d752f274e43 100644 --- a/cli/src/test/java/bisq/cli/opts/OptionParsersTest.java +++ b/cli/src/test/java/bisq/cli/opts/OptionParsersTest.java @@ -219,12 +219,12 @@ public void testCreateCryptoCurrencyPaymentAcctOptionParserWithInvalidCurrencyCo String[] args = new String[]{ PASSWORD_OPT, createcryptopaymentacct.name(), - "--" + OPT_ACCOUNT_NAME + "=" + "bsq payment account", - "--" + OPT_CURRENCY_CODE + "=" + "xmr" + "--" + OPT_ACCOUNT_NAME + "=" + "bch payment account", + "--" + OPT_CURRENCY_CODE + "=" + "bch" }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); - assertEquals("api only supports bsq crypto currency payment accounts", exception.getMessage()); + assertEquals("api does not support bch payment accounts", exception.getMessage()); } @Test @@ -232,12 +232,12 @@ public void testCreateCryptoCurrencyPaymentAcctOptionParserWithMissingAddressOpt String[] args = new String[]{ PASSWORD_OPT, createcryptopaymentacct.name(), - "--" + OPT_ACCOUNT_NAME + "=" + "bsq payment account", - "--" + OPT_CURRENCY_CODE + "=" + "bsq" + "--" + OPT_ACCOUNT_NAME + "=" + "xmr payment account", + "--" + OPT_CURRENCY_CODE + "=" + "xmr" }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); - assertEquals("no bsq address specified", exception.getMessage()); + assertEquals("no xmr address specified", exception.getMessage()); } @Test diff --git a/cli/src/test/java/bisq/cli/table/AbstractSmokeTest.java b/cli/src/test/java/bisq/cli/table/AbstractSmokeTest.java new file mode 100644 index 00000000000..5d43dd7d5c6 --- /dev/null +++ b/cli/src/test/java/bisq/cli/table/AbstractSmokeTest.java @@ -0,0 +1,103 @@ +package bisq.cli.table; + +import joptsimple.OptionParser; +import joptsimple.OptionSet; + +import java.util.LinkedList; +import java.util.function.Predicate; + +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.err; +import static java.lang.System.out; +import static org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch.Operation.DELETE; +import static org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch.Operation.INSERT; + + + +import bisq.cli.GrpcClient; +import bisq.cli.opts.ArgumentList; +import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch; + +/** + * Smoke tests for CLI methods. Useful for examining the format of the console output, + * and checking for diffs while making changes to console output formatters. + * + * These are not jupiter/unit tests that run during a gradle build. + * + * Tests that create offers or trades should not be run on mainnet. + */ +abstract class AbstractSmokeTest { + + 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 GrpcClient aliceClient; + protected final GrpcClient bobClient; + + AbstractSmokeTest() { + 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 showDiffsIgnoreWhitespace(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; + err.println(">>> DIFF " + d); + } + } + } + + if (hasNonWhitespaceDiffs) + err.println("THERE ARE DIFFS"); + else + out.println("NO DIFFS"); + + err.flush(); + out.flush(); + } + + protected void printOldTbl(String tbl) { + out.println("== OLD OUTPUT TBL =="); + out.println(tbl); + } + + protected void printNewTbl(String tbl) { + out.println("== NEW OUTPUT TBL =="); + out.println(tbl); + } +} diff --git a/cli/src/test/java/bisq/cli/table/AddressSmokeTest.java b/cli/src/test/java/bisq/cli/table/AddressSmokeTest.java new file mode 100644 index 00000000000..3aa66e71ce1 --- /dev/null +++ b/cli/src/test/java/bisq/cli/table/AddressSmokeTest.java @@ -0,0 +1,59 @@ +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.table.builder.TableBuilder; + +@SuppressWarnings("unused") +public class AddressSmokeTest extends AbstractSmokeTest { + + public static void main(String[] args) { + AddressSmokeTest test = new AddressSmokeTest(); + test.getFundingAddresses(); + test.getAddressBalance(); + } + + public AddressSmokeTest() { + super(); + } + + private void getFundingAddresses() { + var fundingAddresses = aliceClient.getFundingAddresses(); + if (fundingAddresses.size() > 0) { + // var oldTbl = TODO + var newTbl = new TableBuilder(ADDRESS_BALANCE_TBL, fundingAddresses).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + // showDiffsIgnoreWhitespace(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)); + // var oldTbl = TODO + var newTbl = new TableBuilder(ADDRESS_BALANCE_TBL, addressBalance).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + // showDiffsIgnoreWhitespace(oldTbl, newTbl); + } +} diff --git a/cli/src/test/java/bisq/cli/table/GetBalanceSmokeTest.java b/cli/src/test/java/bisq/cli/table/GetBalanceSmokeTest.java new file mode 100644 index 00000000000..6de1423c130 --- /dev/null +++ b/cli/src/test/java/bisq/cli/table/GetBalanceSmokeTest.java @@ -0,0 +1,40 @@ +package bisq.cli.table; + +import static bisq.cli.table.builder.TableType.BSQ_BALANCE_TBL; +import static bisq.cli.table.builder.TableType.BTC_BALANCE_TBL; + + + +import bisq.cli.table.builder.TableBuilder; + +@SuppressWarnings("unused") +public class GetBalanceSmokeTest extends AbstractSmokeTest { + + public static void main(String[] args) { + GetBalanceSmokeTest test = new GetBalanceSmokeTest(); + test.getBtcBalance(); + test.getBsqBalance(); + } + + public GetBalanceSmokeTest() { + super(); + } + + private void getBtcBalance() { + var balance = aliceClient.getBtcBalances(); + // var oldTbl = TODO + var newTbl = new TableBuilder(BTC_BALANCE_TBL, balance).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + // showDiffsIgnoreWhitespace(oldTbl, newTbl); + } + + private void getBsqBalance() { + var balance = aliceClient.getBsqBalances(); + // var oldTbl = TODO + var newTbl = new TableBuilder(BSQ_BALANCE_TBL, balance).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + // showDiffsIgnoreWhitespace(oldTbl, newTbl); + } +} diff --git a/cli/src/test/java/bisq/cli/table/GetOffersSmokeTest.java b/cli/src/test/java/bisq/cli/table/GetOffersSmokeTest.java new file mode 100644 index 00000000000..0926cbc7a89 --- /dev/null +++ b/cli/src/test/java/bisq/cli/table/GetOffersSmokeTest.java @@ -0,0 +1,121 @@ +package bisq.cli.table; + +import bisq.proto.grpc.OfferInfo; + +import java.util.List; + +import static bisq.cli.table.builder.TableType.OFFER_TBL; +import static java.lang.String.format; +import static java.lang.System.out; +import static protobuf.OfferPayload.Direction.BUY; +import static protobuf.OfferPayload.Direction.SELL; + + + +import bisq.cli.table.builder.TableBuilder; + +@SuppressWarnings("unused") +public class GetOffersSmokeTest extends AbstractSmokeTest { + + // "My" offers are always Alice's offers. + // "Available" offers are always Alice's offers available to Bob. + + public static void main(String[] args) { + GetOffersSmokeTest test = new GetOffersSmokeTest(); + + test.getMyBuyUsdOffers(); + test.getMySellUsdOffers(); + test.getAvailableBuyUsdOffers(); + test.getAvailableSellUsdOffers(); + + test.getMyBuyXmrOffers(); + test.getMySellXmrOffers(); + test.getAvailableBuyXmrOffers(); + test.getAvailableSellXmrOffers(); + + test.getMyBuyBsqOffers(); + test.getMySellBsqOffers(); + test.getAvailableBuyBsqOffers(); + test.getAvailableSellBsqOffers(); + } + + public GetOffersSmokeTest() { + 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()) { + out.println(format("No %s %s offers to print.", direction, currencyCode)); + } else { + // out.println(format("Checking for diffs in %s %s offers.", direction, currencyCode)); + // var oldTbl = TODO + var newTbl = new TableBuilder(OFFER_TBL, offers).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + out.flush(); + // showDiffsIgnoreWhitespace(oldTbl, newTbl); + } + } +} diff --git a/cli/src/test/java/bisq/cli/table/GetTradeSmokeTest.java b/cli/src/test/java/bisq/cli/table/GetTradeSmokeTest.java new file mode 100644 index 00000000000..7183c649a7b --- /dev/null +++ b/cli/src/test/java/bisq/cli/table/GetTradeSmokeTest.java @@ -0,0 +1,47 @@ +package bisq.cli.table; + +import static bisq.cli.table.builder.TableType.TRADE_TBL; +import static java.lang.System.out; + + + +import bisq.cli.GrpcClient; +import bisq.cli.table.builder.TableBuilder; + +@SuppressWarnings("unused") +public class GetTradeSmokeTest extends AbstractSmokeTest { + + public static void main(String[] args) { + if (args.length == 0) + throw new IllegalStateException("Need a single trade-id program argument."); + + GetTradeSmokeTest test = new GetTradeSmokeTest(args[0]); + test.getAlicesTrade(); + out.println(); + test.getBobsTrade(); + } + + private final String tradeId; + + public GetTradeSmokeTest(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); + // var oldTbl = TODO + var newTbl = new TableBuilder(TRADE_TBL, trade).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + // showDiffsIgnoreWhitespace(oldTbl, newTbl); + } +} diff --git a/cli/src/test/java/bisq/cli/table/GetTransactionSmokeTest.java b/cli/src/test/java/bisq/cli/table/GetTransactionSmokeTest.java new file mode 100644 index 00000000000..f7971358ede --- /dev/null +++ b/cli/src/test/java/bisq/cli/table/GetTransactionSmokeTest.java @@ -0,0 +1,36 @@ +package bisq.cli.table; + +import static bisq.cli.table.builder.TableType.TRANSACTION_TBL; + + + +import bisq.cli.table.builder.TableBuilder; + +@SuppressWarnings("unused") +public class GetTransactionSmokeTest extends AbstractSmokeTest { + + public static void main(String[] args) { + if (args.length == 0) + throw new IllegalStateException("Need a single transaction-id program argument."); + + GetTransactionSmokeTest test = new GetTransactionSmokeTest(args[0]); + test.getTransaction(); + } + + private final String transactionId; + + public GetTransactionSmokeTest(String transactionId) { + super(); + this.transactionId = transactionId; + } + + private void getTransaction() { + var tx = aliceClient.getTransaction(transactionId); + // var oldTbl = TODO + var newTbl = new TableBuilder(TRANSACTION_TBL, tx).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + // Should show 1 diff due to new 'Is Confirmed' column being left justified (fixed). + // showDiffsIgnoreWhitespace(oldTbl, newTbl); + } +} diff --git a/cli/src/test/java/bisq/cli/table/PaymentAccountsSmokeTest.java b/cli/src/test/java/bisq/cli/table/PaymentAccountsSmokeTest.java new file mode 100644 index 00000000000..37dd9394899 --- /dev/null +++ b/cli/src/test/java/bisq/cli/table/PaymentAccountsSmokeTest.java @@ -0,0 +1,35 @@ +package bisq.cli.table; + +import static bisq.cli.table.builder.TableType.PAYMENT_ACCOUNT_TBL; +import static java.lang.System.out; + + + +import bisq.cli.table.builder.TableBuilder; + +@SuppressWarnings("unused") +public class PaymentAccountsSmokeTest extends AbstractSmokeTest { + + public static void main(String[] args) { + PaymentAccountsSmokeTest test = new PaymentAccountsSmokeTest(); + test.getPaymentAccounts(); + } + + public PaymentAccountsSmokeTest() { + super(); + } + + private void getPaymentAccounts() { + var paymentAccounts = aliceClient.getPaymentAccounts(); + if (paymentAccounts.size() > 0) { + // var oldTbl = TODO + var newTbl = new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccounts).build().toString(); + // printOldTbl(oldTbl); + printNewTbl(newTbl); + // showDiffsIgnoreWhitespace(oldTbl, newTbl); + } else { + out.println("no payment accounts are saved"); + } + } + +} diff --git a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java index 416dde6531b..7dc718656ed 100644 --- a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java @@ -27,6 +27,9 @@ import bisq.core.payment.payload.PaymentMethod; import bisq.core.user.User; +import bisq.asset.Asset; +import bisq.asset.AssetRegistry; + import javax.inject.Inject; import javax.inject.Singleton; @@ -36,10 +39,14 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import static bisq.common.app.DevEnv.isDaoTradingActivated; +import static bisq.common.config.Config.baseCurrencyNetwork; +import static bisq.core.locale.CurrencyUtil.findAsset; import static bisq.core.locale.CurrencyUtil.getCryptoCurrency; import static java.lang.String.format; @@ -103,12 +110,9 @@ PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, String currencyCode, String address, boolean tradeInstant) { - String bsqCode = currencyCode.toUpperCase(); - if (!bsqCode.equals("BSQ")) - throw new IllegalArgumentException("api does not currently support " + currencyCode + " accounts"); - - // Validate the BSQ address string but ignore the return value. - coreWalletsService.getValidBsqAddress(address); + String cryptoCurrencyCode = currencyCode.toUpperCase(); + verifyApiDoesSupportCryptoCurrencyAccount(cryptoCurrencyCode); + verifyCryptoCurrencyAddress(cryptoCurrencyCode, address); var cryptoCurrencyAccount = tradeInstant ? (InstantCryptoCurrencyAccount) PaymentAccountFactory.getPaymentAccount(PaymentMethod.BLOCK_CHAINS_INSTANT) @@ -116,7 +120,7 @@ PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, cryptoCurrencyAccount.init(); cryptoCurrencyAccount.setAccountName(accountName); cryptoCurrencyAccount.setAddress(address); - Optional cryptoCurrency = getCryptoCurrency(bsqCode); + Optional cryptoCurrency = getCryptoCurrency(cryptoCurrencyCode); cryptoCurrency.ifPresent(cryptoCurrencyAccount::setSingleTradeCurrency); user.addPaymentAccount(cryptoCurrencyAccount); accountAgeWitnessService.publishMyAccountAgeWitness(cryptoCurrencyAccount.getPaymentAccountPayload()); @@ -137,6 +141,41 @@ List getCryptoCurrencyPaymentMethods() { .collect(Collectors.toList()); } + private void verifyCryptoCurrencyAddress(String cryptoCurrencyCode, String address) { + if (cryptoCurrencyCode.equals("BSQ")) { + // Validate the BSQ address, but ignore the return value. + coreWalletsService.getValidBsqAddress(address); + } else { + Asset asset = getAsset(cryptoCurrencyCode); + if (!asset.validateAddress(address).isValid()) + throw new IllegalArgumentException( + format("%s is not a valid %s address", + address, + cryptoCurrencyCode.toLowerCase())); + } + } + + private final Predicate apiDoesSupportCryptoCurrencyAccount = (c) -> + c.equals("BSQ") || c.equals("XMR"); + + private void verifyApiDoesSupportCryptoCurrencyAccount(String cryptoCurrencyCode) { + if (!apiDoesSupportCryptoCurrencyAccount.test(cryptoCurrencyCode)) + throw new IllegalArgumentException( + format("api does not currently support %s accounts", + cryptoCurrencyCode.toLowerCase())); + + } + + private Asset getAsset(String cryptoCurrencyCode) { + return findAsset(new AssetRegistry(), + cryptoCurrencyCode, + baseCurrencyNetwork(), + isDaoTradingActivated()) + .orElseThrow(() -> new IllegalStateException( + format("crypto currency with code '%s' not found", + cryptoCurrencyCode.toLowerCase()))); + } + private void verifyPaymentAccountHasRequiredFields(PaymentAccount paymentAccount) { if (!paymentAccount.hasMultipleCurrencies() && paymentAccount.getSingleTradeCurrency() == null) throw new IllegalArgumentException(format("no trade currency defined for %s payment account", diff --git a/core/src/main/java/bisq/core/api/CorePriceService.java b/core/src/main/java/bisq/core/api/CorePriceService.java index 4553689e98a..a0c62ba1ca1 100644 --- a/core/src/main/java/bisq/core/api/CorePriceService.java +++ b/core/src/main/java/bisq/core/api/CorePriceService.java @@ -23,10 +23,12 @@ import javax.inject.Singleton; import java.util.function.Consumer; +import java.util.function.Predicate; import lombok.extern.slf4j.Slf4j; import static bisq.common.util.MathUtils.roundDouble; +import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; import static bisq.core.locale.CurrencyUtil.isFiatCurrency; import static java.lang.String.format; @@ -34,6 +36,8 @@ @Slf4j class CorePriceService { + private final Predicate isCurrencyCode = (c) -> isFiatCurrency(c) || isCryptoCurrency(c); + private final PriceFeedService priceFeedService; @Inject @@ -44,7 +48,7 @@ public CorePriceService(PriceFeedService priceFeedService) { public void getMarketPrice(String currencyCode, Consumer resultHandler) { String upperCaseCurrencyCode = currencyCode.toUpperCase(); - if (!isFiatCurrency(upperCaseCurrencyCode)) + if (!isCurrencyCode.test(upperCaseCurrencyCode)) throw new IllegalStateException(format("%s is not a valid currency code", upperCaseCurrencyCode)); if (!priceFeedService.hasPrices()) @@ -59,11 +63,18 @@ public void getMarketPrice(String currencyCode, Consumer resultHandler) priceFeedService.requestPriceFeed(price -> { if (price > 0) { log.info("{} price feed request returned {}", upperCaseCurrencyCode, price); - resultHandler.accept(roundDouble(price, 4)); + if (isFiatCurrency(upperCaseCurrencyCode)) + resultHandler.accept(roundDouble(price, 4)); + else if (isCryptoCurrency(upperCaseCurrencyCode)) + resultHandler.accept(roundDouble(price, 8)); + else // should not happen, throw error if it does + throw new IllegalStateException( + format("%s price feed request should not return data for unsupported currency code", + upperCaseCurrencyCode)); } else { throw new IllegalStateException(format("%s price is not available", upperCaseCurrencyCode)); } }, - (errorMessage, throwable) -> log.warn(errorMessage, throwable)); + log::warn); } } diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index 54f841e125c..e16822e80fa 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -82,7 +82,6 @@ import javax.annotation.Nullable; -import static bisq.common.config.BaseCurrencyNetwork.BTC_DAO_REGTEST; import static bisq.core.btc.wallet.Restrictions.getMinNonDustOutput; import static bisq.core.util.ParsingUtils.parseToCoin; import static java.lang.String.format; @@ -210,10 +209,10 @@ List getFundingAddresses() { } return addressStrings.stream().map(address -> - new AddressBalanceInfo(address, - balances.getUnchecked(address), - getNumConfirmationsForMostRecentTransaction(address), - btcWalletService.isAddressUnused(getAddressEntry(address).getAddress()))) + new AddressBalanceInfo(address, + balances.getUnchecked(address), + getNumConfirmationsForMostRecentTransaction(address), + btcWalletService.isAddressUnused(getAddressEntry(address).getAddress()))) .collect(Collectors.toList()); } @@ -552,7 +551,7 @@ Address getValidBsqAddress(String address) { return bsqFormatter.getAddressFromBsqAddress(address); } catch (RuntimeException e) { log.error("", e); - throw new IllegalStateException(format("%s is not a valid bsq address", address)); + throw new IllegalArgumentException(format("%s is not a valid bsq address", address)); } } diff --git a/core/src/main/java/bisq/core/api/EditOfferValidator.java b/core/src/main/java/bisq/core/api/EditOfferValidator.java index 7a9840c3cdb..3df569e315f 100644 --- a/core/src/main/java/bisq/core/api/EditOfferValidator.java +++ b/core/src/main/java/bisq/core/api/EditOfferValidator.java @@ -8,7 +8,6 @@ import lombok.extern.slf4j.Slf4j; -import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; import static java.lang.String.format; @Slf4j @@ -62,7 +61,7 @@ void validate() { case TRIGGER_PRICE_AND_ACTIVATION_STATE: case MKT_PRICE_MARGIN_AND_TRIGGER_PRICE: case MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE: { - checkNotAltcoinOffer(); + checkNotBsqOffer(); validateEditedTriggerPrice(); validateEditedMarketPriceMargin(); break; @@ -119,9 +118,7 @@ private void validateEditedTriggerPrice() { && !editedUseMarketBasedPrice && !isZeroEditedTriggerPrice) throw new IllegalStateException( - format("programmer error: cannot set a trigger price (%s)" - + " in fixed price offer with id '%s'", - editedTriggerPrice, + format("programmer error: cannot set a trigger price in fixed price offer with id '%s'", currentlyOpenOffer.getId())); if (editedTriggerPrice < 0) @@ -131,10 +128,10 @@ private void validateEditedTriggerPrice() { currentlyOpenOffer.getId())); } - private void checkNotAltcoinOffer() { - if (isCryptoCurrency(currentlyOpenOffer.getOffer().getCurrencyCode())) { + private void checkNotBsqOffer() { + if ("BSQ".equals(currentlyOpenOffer.getOffer().getCurrencyCode())) { throw new IllegalStateException( - format("cannot set mkt price margin or trigger price on fixed price altcoin offer with id '%s'", + format("cannot set mkt price margin or trigger price on fixed price bsq offer with id '%s'", currentlyOpenOffer.getId())); } } diff --git a/core/src/main/java/bisq/core/payment/payload/AssetsAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/AssetsAccountPayload.java index e35dd4c7bcb..092a72df378 100644 --- a/core/src/main/java/bisq/core/payment/payload/AssetsAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/AssetsAccountPayload.java @@ -26,11 +26,9 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; -import lombok.ToString; import lombok.extern.slf4j.Slf4j; @EqualsAndHashCode(callSuper = true) -@ToString @Setter @Getter @Slf4j @@ -77,4 +75,15 @@ public String getPaymentDetailsForTradePopup() { public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(address.getBytes(StandardCharsets.UTF_8)); } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "{" + + "id='" + id + '\'' + + ", paymentMethodId='" + paymentMethodId + '\'' + + ", address='" + address + '\'' + + ", maxTradePeriod=" + maxTradePeriod + + ", excludeFromJsonDataMap=" + excludeFromJsonDataMap + + '}'; + } } diff --git a/core/src/main/resources/help/createcryptopaymentacct-help.txt b/core/src/main/resources/help/createcryptopaymentacct-help.txt index 83fa2e9f633..4c377bb401b 100644 --- a/core/src/main/resources/help/createcryptopaymentacct-help.txt +++ b/core/src/main/resources/help/createcryptopaymentacct-help.txt @@ -8,13 +8,13 @@ SYNOPSIS -------- createcryptopaymentacct --account-name= - --currency-code= - --address= + --currency-code= + --address= [--trade-instant=] DESCRIPTION ----------- -Create an cryptocurrency (altcoin) payment account. Only BSQ payment accounts are currently supported. +Create an cryptocurrency (altcoin) payment account. Only BSQ and XMR payment accounts are currently supported. OPTIONS ------- @@ -22,7 +22,7 @@ OPTIONS The name of the cryptocurrency payment account used to create and take altcoin offers. --currency-code - The three letter code for the altcoin, e.g., BSQ. + The three-letter code for the altcoin, e.g., BSQ. --address The altcoin address to be used receive cryptocurrency payment when selling BTC. @@ -40,8 +40,8 @@ $ ./bisq-cli --password=xyz --port=9998 createcryptopaymentacct --account-name=" --address=Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne \ --trade-instant=false -To create a BSQ Instant Altcoin payment account: -$ ./bisq-cli --password=xyz --port=9998 createcryptopaymentacct --account-name="My Instant BSQ Account" \ - --currency-code=bsq \ - --address=Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne \ +To create an XMR Instant Altcoin payment account: +$ ./bisq-cli --password=xyz --port=9998 createcryptopaymentacct --account-name="My Instant XMR Account" \ + --currency-code=xmr \ + --address=42eKMxqFEexaHUB5KScZvVXBHYcrn5wo4Tm6Zp1PoHyvieRkZdkg7rFRm3Z2KNZ28vZzFrpTcSTpQa8m7LJXv97ARfSbPJo \ --trade-instant=true diff --git a/core/src/main/resources/help/createoffer-help.txt b/core/src/main/resources/help/createoffer-help.txt index 38ec0fd8daf..2cdbef2dd03 100644 --- a/core/src/main/resources/help/createoffer-help.txt +++ b/core/src/main/resources/help/createoffer-help.txt @@ -9,7 +9,7 @@ SYNOPSIS createoffer --payment-account= --direction= - --currency-code= + --currency-code= --market-price-margin= | --fixed-price= --amount= --min-amount= @@ -18,25 +18,25 @@ createoffer DESCRIPTION ----------- -Create and place an offer to buy or sell BTC using a fiat account. +Create and place an offer to buy or sell BTC using a fiat or altcoin account. OPTIONS ------- --payment-account - The ID of the fiat payment account used to send or receive funds during the trade. + The ID of the payment account used to send or receive funds during the trade. --direction The direction of the trade (BUY or SELL). --currency-code - The three letter code for the fiat used to buy or sell BTC, e.g., EUR, USD, BRL, ... + The three-letter code for the fiat or altcoin used to buy or sell BTC, e.g., BSQ, XMR, EUR, USD, ... --market-price-margin The % above or below market BTC price, e.g., 1.00 (1%). If --market-price-margin is not present, --fixed-price must be. --fixed-price - The fixed BTC price in fiat used to buy or sell BTC, e.g., 34000 (USD). + The fixed BTC price used to buy or sell BTC, e.g., 45000 (USD). If --fixed-price is not present, --market-price-margin must be. --amount @@ -80,3 +80,29 @@ $ ./bisq-cli --password=xyz --port=9998 createoffer --payment-account=7413d263-2 --fixed-price=40000 \ --security-deposit=25.0 \ --fee-currency=btc + +To create a BUY 0.006 BTC for BSQ offer + at a fixed BSQ price of 0.00005 BTC, + using a payment account with ID 1473d263-225a-4f1c-837a-1e3094dc0e32, + putting up a 25 percent security deposit, + and paying the Bisq maker trading fee in BTC: +$ ./bisq-cli --password=xyz --port=9998 createoffer --payment-account=1473d263-225a-4f1c-837a-1e3094dc0e32 \ + --direction=buy \ + --currency-code=bsq \ + --amount=0.006 \ + --fixed-price=0.00005 \ + --security-deposit=25.0 \ + --fee-currency=btc + +To create a SELL 0.025 BTC for XMR offer + at a market price margin of 0.50 percent above current XMR market price, + using a payment account with ID 1373d263-225a-4f1b-837a-1e3094dc0e32, + putting up a 25 percent security deposit, + and paying the Bisq maker trading fee in BSQ: +$ ./bisq-cli --password=xyz --port=9998 createoffer --payment-account=1373d263-225a-4f1b-837a-1e3094dc0e32 \ + --direction=sell \ + --currency-code=xmr \ + --amount=0.025 \ + --market-price-margin=0.50 \ + --security-deposit=25.0 \ + --fee-currency=bsq diff --git a/core/src/main/resources/help/takeoffer-help.txt b/core/src/main/resources/help/takeoffer-help.txt index 1290a008392..a0b7aae1d2d 100644 --- a/core/src/main/resources/help/takeoffer-help.txt +++ b/core/src/main/resources/help/takeoffer-help.txt @@ -9,7 +9,7 @@ SYNOPSIS takeoffer --offer-id= --payment-account= - --fee-currency= + --fee-currency= DESCRIPTION ----------- @@ -21,7 +21,7 @@ OPTIONS The ID of the buy or sell offer to take. --payment-account - The ID of the fiat payment account used to send or receive funds during the trade. + The ID of the fiat or altcoin payment account used to send or receive funds during the trade. The payment account's payment method must match that of the offer. --fee-currency