diff --git a/apitest/scripts/trade-simulation-env.sh b/apitest/scripts/trade-simulation-env.sh index e3735fe2a99..a3f7f7425e4 100755 --- a/apitest/scripts/trade-simulation-env.sh +++ b/apitest/scripts/trade-simulation-env.sh @@ -214,6 +214,51 @@ parsebsqswaporderopts() { export CURRENCY_CODE="BSQ" } +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 4b63c3a7c5a..91e5f2bdd97 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" @@ -531,7 +531,7 @@ executetrade() { fi # Generate some btc blocks - printdate "Generating btc blocks after fiat transfer." + printdate "Generating btc blocks after payment." genbtcblocks 2 2 printbreak diff --git a/apitest/scripts/trade-xmr-simulation.sh b/apitest/scripts/trade-xmr-simulation.sh new file mode 100755 index 00000000000..8d486496350 --- /dev/null +++ b/apitest/scripts/trade-xmr-simulation.sh @@ -0,0 +1,122 @@ +#! /bin/bash + +# Runs xmr <-> btc trading scenarios using the API CLI with a local regtest bitcoin node. +# +# Prerequisites: +# +# - Linux or OSX with bash, Java 11-15 (JDK language compatibility 11), and bitcoin-core (v0.19 - v22). +# +# - Bisq must be fully built with apitest dao setup files installed. +# Build command: `./gradlew clean build :apitest:installDaoSetup` +# +# - All supporting nodes must be run locally, in dev/dao/regtest mode: +# bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon +# +# These should be run using the apitest harness. From the root project dir, run: +# `$ ./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false` +# +# Usage: +# +# This script must be run from the root of the project, e.g.: +# +# `$ apitest/scripts/trade-xmr-simulation.sh -d buy -f 0.05 -a 0.125` +# +# Script options: -d -m -f -a +# +# Examples: +# +# Create a buy/xmr offer to buy 0.125 btc at an xmr fixed-price of 0.05 btc, using an xmr payment account: +# +# `$ apitest/scripts/trade-xmr-simulation.sh -d buy -f 0.05 -a 0.125` +# +# Create a sell/xmr offer to sell 0.125 btc at at an xmr mkt-price-margin of 0%, using using an xmr payment account: +# +# `$ apitest/scripts/trade-xmr-simulation.sh -d sell -m 0.00 -a 0.125` + +export APP_BASE_NAME=$(basename "$0") +export APP_HOME=$(pwd -P) +export APITEST_SCRIPTS_HOME="$APP_HOME/apitest/scripts" +export CURRENCY_CODE="XMR" +export ALICE_XMR_ADDRESS="44i8xZbd8ecaD6nQQrHjr1BwTp6QfGL22iWqHZKmU4QYSyr1F64XAxM4HgvQHxbny7ehfxemaA9LPDLz2wY3fxhB1bbMEco" +export BOB_XMR_ADDRESS="48xdBkXaCosPxcWwXRZdSGc33M9tYu6k9ga56dqkNrgsjQuJX16xW2qTyWTZstJpXXj87dj5p4H3y1xAfoVjAysoAYrXh2N" + +source "$APITEST_SCRIPTS_HOME/trade-simulation-env.sh" +source "$APITEST_SCRIPTS_HOME/trade-simulation-utils.sh" + +checksetup +parsexmrscriptopts "$@" + +printdate "Started $APP_BASE_NAME with parameters:" +printscriptparams +printbreak + +registerdisputeagents + +# Demonstrate how to create an XMR altcoin payment account. + +printdate "Create Alice's XMR Trading Payment Account." +# Note: Having problems passing a double quoted --account-name param to function. +CMD="$CLI_BASE --port=$ALICE_PORT createcryptopaymentacct --account-name=Alice_XMR_Account" +CMD+=" --currency-code=XMR --address=$ALICE_XMR_ADDRESS --trade-instant=false" +printdate "ALICE CLI: $CMD" +CMD_OUTPUT=$(createpaymentacct "$CMD") +echo "$CMD_OUTPUT" +printbreak +export ALICE_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") +printdate "Alice's XMR payment-account-id: $ALICE_ACCT_ID" +exitoncommandalert $? +printbreak + +printdate "Create Bob's XMR Trading Payment Account." +# Note: Having problems passing a double quoted --account-name param to function. +CMD="$CLI_BASE --port=$BOB_PORT createcryptopaymentacct --account-name=Bob_XMR_Account" +CMD+=" --currency-code=XMR --address=$BOB_XMR_ADDRESS --trade-instant=false" +printdate "BOB CLI: $CMD" +CMD_OUTPUT=$(createpaymentacct "$CMD") +echo "$CMD_OUTPUT" +printbreak +export BOB_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") +printdate "Bob's XMR payment-account-id: $BOB_ACCT_ID" +exitoncommandalert $? +printbreak + +# Alice creates an offer. +printdate "ALICE $ALICE_ROLE: Creating $DIRECTION $CURRENCY_CODE offer with payment acct $ALICE_ACCT_ID." +CMD=$(gencreateoffercommand "$ALICE_PORT" "$ALICE_ACCT_ID") +printdate "ALICE CLI: $CMD" +OFFER_ID=$(createoffer "$CMD") +exitoncommandalert $? +printdate "ALICE $ALICE_ROLE: Created offer with id: $OFFER_ID." +printbreak +sleeptraced 3 + +# Show Alice's new offer. +printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer." +CMD="$CLI_BASE --port=$ALICE_PORT 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/test/java/bisq/apitest/method/offer/AbstractOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java index 218af0e15e1..addd0733453 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java @@ -35,6 +35,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; @@ -65,6 +66,9 @@ public abstract class AbstractOfferTest extends MethodTest { protected static PaymentAccount alicesLegacyBsqAcct; protected static PaymentAccount bobsLegacyBsqAcct; + protected static PaymentAccount alicesXmrAcct; + protected static PaymentAccount bobsXmrAcct; + @BeforeAll public static void setUp() { startSupportingApps(true, @@ -96,11 +100,16 @@ 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))); @@ -113,6 +122,7 @@ public static void setUp() { protected final Function, String> toOffersTable = (offers) -> new TableBuilder(OFFER_TBL, offers).build().toString(); + @SuppressWarnings("ConstantConditions") public static void initSwapPaymentAccounts() { // A bot may not know what the default 'BSQ Swap' account name is, // but API test cases do: the value of the i18n property 'BSQ_SWAP'. @@ -132,6 +142,20 @@ public static void createLegacyBsqPaymentAccounts() { false); } + @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 public static void tearDown() { tearDownScaffold(); 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 72a5de9e56b..0f2c9702845 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateBSQOffersTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateBSQOffersTest.java @@ -245,7 +245,7 @@ public void testCreateSellBTCFor5To10KBSQOffer() { @Test @Order(5) public void testGetAllMyBsqOffers() { - List offers = aliceClient.getMyBsqOffersSortedByDate(); + List offers = aliceClient.getMyCryptoCurrencyOffersSortedByDate(BSQ); log.debug("ALL ALICE'S BSQ OFFERS:\n{}", toOffersTable.apply(offers)); assertEquals(4, offers.size()); log.debug("ALICE'S BALANCES\n{}", formatBalancesTbls(aliceClient.getBalances())); @@ -254,7 +254,7 @@ public void testGetAllMyBsqOffers() { @Test @Order(6) public void testGetAvailableBsqOffers() { - List offers = bobClient.getBsqOffersSortedByDate(); + List offers = bobClient.getCryptoCurrencyOffersSortedByDate(BSQ); log.debug("ALL BOB'S AVAILABLE BSQ OFFERS:\n{}", toOffersTable.apply(offers)); assertEquals(4, offers.size()); log.debug("BOB'S BALANCES\n{}", formatBalancesTbls(bobClient.getBalances())); 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..6cf609498a5 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateXMROffersTest.java @@ -0,0 +1,282 @@ +/* + * 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.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.OfferDirection.BUY; +import static protobuf.OfferDirection.SELL; + +@SuppressWarnings("ConstantConditions") +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class CreateXMROffersTest extends AbstractOfferTest { + + private static final String MAKER_FEE_CURRENCY_CODE = 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.debug("Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer)); + 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.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer)); + 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.debug("Pending Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer)); + 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.debug("Available Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer)); + 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.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer)); + 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.debug("All of Alice's XMR offers:\n{}", toOffersTable.apply(offers)); + assertEquals(4, offers.size()); + log.debug("Alice's balances\n{}", formatBalancesTbls(aliceClient.getBalances())); + } + + @Test + @Order(6) + public void testGetAvailableXMROffers() { + List offers = bobClient.getCryptoCurrencyOffersSortedByDate(XMR); + log.debug("All of Bob's available XMR offers:\n{}", toOffersTable.apply(offers)); + assertEquals(4, offers.size()); + log.debug("Bob's balances\n{}", formatBalancesTbls(bobClient.getBalances())); + } + + private void genBtcBlockAndWaitForOfferPreparation() { + // 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 559f137882a..70e7ce7a962 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java @@ -37,10 +37,7 @@ 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.EUR; -import static bisq.apitest.config.ApiTestConfig.USD; +import static bisq.apitest.config.ApiTestConfig.*; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; import static bisq.proto.grpc.EditOfferRequest.EditType.*; import static java.lang.String.format; @@ -79,12 +76,14 @@ public void testOfferDisableAndEnable() { OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); log.debug("Edited EUR offer:\n{}", toOfferTable.apply(editedOffer)); assertFalse(editedOffer.getIsActivated()); + assertTrue(editedOffer.getUseMarketBasedPrice()); // Re-enable offer aliceClient.editOfferActivationState(editedOffer.getId(), ACTIVATE_OFFER); genBtcBlocksThenWait(1, 1500); // Wait for offer book re-entry. editedOffer = aliceClient.getMyOffer(originalOffer.getId()); log.debug("Edited EUR offer:\n{}", toOfferTable.apply(editedOffer)); assertTrue(editedOffer.getIsActivated()); + assertTrue(editedOffer.getUseMarketBasedPrice()); doSanityCheck(originalOffer, editedOffer); } @@ -106,7 +105,7 @@ public void testEditTriggerPrice() { // Edit the offer's trigger price, nothing else. 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(2_500); // Wait for offer book re-entry. @@ -157,6 +156,7 @@ public void testEditMktPriceMargin() { OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); log.debug("Edited USD offer:\n{}", toOfferTable.apply(editedOffer)); assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); + assertTrue(editedOffer.getUseMarketBasedPrice()); doSanityCheck(originalOffer, editedOffer); } @@ -215,6 +215,7 @@ public void testEditFixedPriceAndDeactivation() { var expectedNewFixedPrice = scaledUpFiatOfferPrice.apply(new BigDecimal(editedFixedPriceAsString)); assertEquals(expectedNewFixedPrice, editedOffer.getPrice()); assertFalse(editedOffer.getIsActivated()); + assertFalse(editedOffer.getUseMarketBasedPrice()); doSanityCheck(originalOffer, editedOffer); } @@ -251,6 +252,7 @@ public void testEditMktPriceMarginAndDeactivation() { assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); assertEquals(0, editedOffer.getTriggerPrice()); assertFalse(editedOffer.getIsActivated()); + assertTrue(editedOffer.getUseMarketBasedPrice()); doSanityCheck(originalOffer, editedOffer); } @@ -261,7 +263,7 @@ public void testEditMktPriceMarginAndTriggerPriceAndDeactivation() { PaymentAccount paymentAcct = getOrCreatePaymentAccount("US"); var originalMktPriceMargin = new BigDecimal("0.0").doubleValue(); var mktPriceAsDouble = aliceClient.getBtcPrice(USD); - var originalTriggerPriceAsLong = calcPriceAsLong.apply(mktPriceAsDouble, -5_000.0000); + var originalTriggerPriceAsLong = calcFiatTriggerPriceAsLong.apply(mktPriceAsDouble, -5_000.0000); OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), USD, paymentAcct.getId(), @@ -275,7 +277,7 @@ public void testEditMktPriceMarginAndTriggerPriceAndDeactivation() { // 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(), @@ -341,9 +343,8 @@ public void testEditingTriggerPriceInFixedPriceOfferShouldThrowException() { 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()); } @@ -364,7 +365,7 @@ public void testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice() { // 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, @@ -391,7 +392,7 @@ public void testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt() { 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); + var originalTriggerPriceAsLong = calcFiatTriggerPriceAsLong.apply(mktPriceAsDouble, delta); var originalOffer = createMktPricedOfferForEdit(SELL.name(), "GBP", paymentAcct.getId(), @@ -441,7 +442,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()); } @@ -459,7 +460,7 @@ public void testEditTriggerPriceOnFixedPriceBsqOfferShouldThrowException() { BSQ); log.debug("Original BSQ offer:\n{}", toOfferTable.apply(originalOffer)); genBtcBlocksThenWait(1, 2_500); // Wait for offer book entry. - var newTriggerPriceAsLong = calcPriceAsLong.apply(0.00005, 0.00); + var newTriggerPriceAsLong = calcFiatTriggerPriceAsLong.apply(0.00005, 0.00); Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.editOffer(originalOffer.getId(), "0.00", @@ -469,7 +470,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()); } @@ -572,8 +573,261 @@ public void testEditFixedPriceAndDisableBsqOffer() { assertEquals(0, editedOffer.getTriggerPrice()); } + // 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.debug("Pending XMR offer:\n{}", toOfferTable.apply(originalOffer)); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + + originalOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.debug("Original XMR offer:\n{}", toOfferTable.apply(originalOffer)); + + 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.debug("Edited XMR offer:\n{}", toOfferTable.apply(editedOffer)); + 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 testEditTriggerPriceOnPriceMarginBasedXmrOffer() { + createXmrPaymentAccounts(); + double mktPriceMargin = -0.075d; + OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), + XMR, + alicesXmrAcct.getId(), + mktPriceMargin, + NO_TRIGGER_PRICE); + log.debug("Pending XMR offer:\n{}", toOfferTable.apply(originalOffer)); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + + originalOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("Original XMR offer:\n{}", toOfferTable.apply(originalOffer)); + + double mktPriceAsDouble = aliceClient.getBtcPrice(XMR); + long triggerPriceAsLong = calcAltcoinTriggerPriceAsLong.apply(mktPriceAsDouble, 0.001); + aliceClient.editOffer(originalOffer.getId(), + "0", + true, + mktPriceMargin, + triggerPriceAsLong, + ACTIVATE_OFFER, + TRIGGER_PRICE_ONLY); + // 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{}", toOfferTable.apply(editedOffer)); + assertTrue(editedOffer.getUseMarketBasedPrice()); + assertEquals(scaledDownMktPriceMargin.apply(mktPriceMargin), editedOffer.getMarketPriceMargin()); + assertEquals(triggerPriceAsLong, editedOffer.getTriggerPrice()); + assertTrue(editedOffer.getIsActivated()); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(20) + 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.debug("Pending XMR offer:\n{}", toOfferTable.apply(originalOffer)); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + + originalOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.debug("Original XMR offer:\n{}", toOfferTable.apply(originalOffer)); + + // 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.debug("Edited XMR offer:\n{}", toOfferTable.apply(editedOffer)); + assertTrue(editedOffer.getUseMarketBasedPrice()); + assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); + assertEquals(newTriggerPriceAsLong, editedOffer.getTriggerPrice()); + assertTrue(editedOffer.getIsActivated()); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(21) + 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.debug("Original XMR offer:\n{}", toOfferTable.apply(originalOffer)); + 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(22) + 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.debug("Original BSQ offer:\n{}", toOfferTable.apply(originalOffer)); + 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.debug("Edited XMR offer:\n{}", toOfferTable.apply(editedOffer)); + assertEquals(scaledUpAltcoinOfferPrice.apply(newFixedPriceAsString), editedOffer.getPrice()); + assertTrue(editedOffer.getIsActivated()); + assertMarketBasedPriceFieldsAreIgnored(editedOffer); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(23) + 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.debug("Original XMR offer:\n{}", toOfferTable.apply(originalOffer)); + 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.debug("Edited XMR offer:\n{}", toOfferTable.apply(editedOffer)); + assertFalse(editedOffer.getIsActivated()); + assertEquals(scaledUpAltcoinOfferPrice.apply(fixedPriceAsString), editedOffer.getPrice()); + assertMarketBasedPriceFieldsAreIgnored(editedOffer); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(24) + 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.debug("Original XMR offer:\n{}", toOfferTable.apply(originalOffer)); + 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.debug("Edited XMR offer:\n{}", toOfferTable.apply(editedOffer)); + assertFalse(editedOffer.getIsActivated()); + assertEquals(scaledUpAltcoinOfferPrice.apply(newFixedPriceAsString), editedOffer.getPrice()); + assertMarketBasedPriceFieldsAreIgnored(editedOffer); + + doSanityCheck(originalOffer, editedOffer); + } + + // Edit BSQ Swap Offers (should always be blocked) + + @Test + @Order(25) public void testEditBsqSwapOfferShouldThrowException() { var originalOffer = aliceClient.createBsqSwapOffer(SELL.name(), 1_250_000L, @@ -650,6 +904,12 @@ private void doSanityCheck(OfferInfo originalOffer, OfferInfo editedOffer) { assertEquals(originalOffer.getSellerSecurityDeposit(), editedOffer.getSellerSecurityDeposit()); } + private void assertMarketBasedPriceFieldsAreIgnored(OfferInfo editedOffer) { + assertFalse(editedOffer.getUseMarketBasedPrice()); + assertEquals(0.00, editedOffer.getMarketPriceMargin()); + assertEquals(0, editedOffer.getTriggerPrice()); + } + private PaymentAccount getOrCreatePaymentAccount(String countryCode) { if (paymentAcctCache.containsKey(countryCode)) { return paymentAcctCache.get(countryCode); 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 84f4f9bac21..625bcc0ab18 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java @@ -48,6 +48,15 @@ public static void initStaticFixtures() { EXPECTED_PROTOCOL_STATUS.init(); } + protected final TradeInfo takeAlicesOffer(String offerId, + String paymentAccountId, + String takerFeeCurrencyCode) { + return takeAlicesOffer(offerId, + paymentAccountId, + takerFeeCurrencyCode, + true); + } + protected final TradeInfo takeAlicesOffer(String offerId, String paymentAccountId, String takerFeeCurrencyCode, @@ -241,7 +250,7 @@ protected final void logTrade(Logger log, if (log.isDebugEnabled()) { log.debug(format("%s %s%n%s", testName(testInfo), - description.toUpperCase(), + description, new TableBuilder(TRADE_DETAIL_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 876a053b69d..5d43b935778 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBSQOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBSQOfferTest.java @@ -98,7 +98,7 @@ public void testTakeAlicesSellBTCForBSQOffer(final TestInfo testInfo) { tradeId = trade.getTradeId(); genBtcBlocksThenWait(1, 2_500); - alicesBsqOffers = aliceClient.getMyBsqOffersSortedByDate(); + alicesBsqOffers = aliceClient.getMyCryptoCurrencyOffersSortedByDate(BSQ); assertEquals(0, alicesBsqOffers.size()); waitForDepositConfirmation(log, testInfo, bobClient, trade.getTradeId()); 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..868d5c83b5c --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyXMROfferTest.java @@ -0,0 +1,175 @@ +/* + * 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.model.bisq_v1.Trade.Phase.PAYOUT_PUBLISHED; +import static bisq.core.trade.model.bisq_v1.Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG; +import static bisq.core.trade.model.bisq_v1.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.OfferDirection.SELL; + + + +import bisq.apitest.method.offer.AbstractOfferTest; +import bisq.cli.table.builder.TableBuilder; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TakeBuyXMROfferTest extends AbstractTradeTest { + + // Alice is maker / xmr buyer (btc seller), Bob is taker / xmr seller (btc buyer). + + // 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.debug("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)); + logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId)); + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(2) + public void testBobsConfirmPaymentStarted(final TestInfo testInfo) { + try { + var trade = bobClient.getTrade(tradeId); + + verifyTakerDepositConfirmed(trade); + log.debug("Bob sends XMR payment to Alice for trade {}", trade.getTradeId()); + bobClient.confirmPaymentStarted(trade.getTradeId()); + sleep(3500); + waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId); + + logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Sent)", aliceClient.getTrade(tradeId)); + logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Sent)", bobClient.getTrade(tradeId)); + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(3) + public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) { + try { + waitForSellerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId); + + sleep(2_000); + var trade = aliceClient.getTrade(tradeId); + // If we were trading BSQ, Alice would verify payment has been sent to her + // Bisq / BSQ wallet, but we can do no such checks for XMR payments. + // All XMR transfers are done outside Bisq. + log.debug("Alice verifies XMR payment was received from Bob, for trade {}", trade.getTradeId()); + aliceClient.confirmPaymentReceived(trade.getTradeId()); + sleep(3_000); + + trade = aliceClient.getTrade(tradeId); + assertEquals(OFFER_FEE_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)); + logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Received)", bobClient.getTrade(tradeId)); + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(4) + public void testKeepFunds(final TestInfo testInfo) { + try { + genBtcBlocksThenWait(1, 1_000); + var trade = bobClient.getTrade(tradeId); + logTrade(log, testInfo, "Alice's view before keeping funds", trade); + aliceClient.keepFunds(tradeId); + 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)); + logTrade(log, testInfo, "Bob's Taker/Seller View (Done)", bobClient.getTrade(tradeId)); + 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..247b5320164 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellXMROfferTest.java @@ -0,0 +1,183 @@ +/* + * 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.model.bisq_v1.Trade.Phase.PAYOUT_PUBLISHED; +import static bisq.core.trade.model.bisq_v1.Trade.Phase.WITHDRAWN; +import static bisq.core.trade.model.bisq_v1.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; +import static bisq.core.trade.model.bisq_v1.Trade.State.WITHDRAW_COMPLETED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static protobuf.OfferDirection.BUY; + + + +import bisq.apitest.method.offer.AbstractOfferTest; +import bisq.cli.table.builder.TableBuilder; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TakeSellXMROfferTest extends AbstractTradeTest { + + // Alice is maker / xmr seller (btc buyer), Bob is taker / xmr buyer (btc seller). + + // Maker and Taker fees are in BTC. + private static final String TRADE_FEE_CURRENCY_CODE = BTC; + + private static final String WITHDRAWAL_TX_MEMO = "Bob's trade withdrawal"; + + @BeforeAll + public static void setUp() { + AbstractOfferTest.setUp(); + createXmrPaymentAccounts(); + EXPECTED_PROTOCOL_STATUS.init(); + } + + @Test + @Order(1) + public void testTakeAlicesBuyBTCForXMROffer(final TestInfo testInfo) { + try { + // Alice is going to SELL XMR, but the Offer direction = BUY because it is a + // BTC trade; Alice will BUY BTC for XMR. Alice will send Bob XMR. + // Confused me, but just need to remember there are only BTC offers. + var btcTradeDirection = BUY.name(); + double priceMarginPctInput = 1.50; + var alicesOffer = aliceClient.createMarketBasedPricedOffer(btcTradeDirection, + XMR, + 20_000_000L, + 10_500_000L, + priceMarginPctInput, + getDefaultBuyerSecurityDepositAsPercent(), + alicesXmrAcct.getId(), + TRADE_FEE_CURRENCY_CODE, + NO_TRIGGER_PRICE); + log.debug("Alice's SELL XMR (Buy BTC) offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); + genBtcBlocksThenWait(1, 4000); + var offerId = alicesOffer.getId(); + 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)); + logTrade(log, testInfo, "Bob's Taker/Buyer View", bobClient.getTrade(tradeId)); + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(2) + public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) { + try { + var trade = aliceClient.getTrade(tradeId); + waitForDepositConfirmation(log, testInfo, aliceClient, trade.getTradeId()); + log.debug("Alice sends XMR payment to Bob for trade {}", trade.getTradeId()); + aliceClient.confirmPaymentStarted(trade.getTradeId()); + sleep(3500); + + waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId); + logTrade(log, testInfo, "Alice's Maker/Seller View (Payment Sent)", aliceClient.getTrade(tradeId)); + logTrade(log, testInfo, "Bob's Taker/Buyer View (Payment Sent)", bobClient.getTrade(tradeId)); + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Test + @Order(3) + public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { + try { + waitForSellerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId); + + var trade = bobClient.getTrade(tradeId); + sleep(2_000); + // If we were trading 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.debug("Bob verifies XMR payment was received from Alice, for trade {}", trade.getTradeId()); + bobClient.confirmPaymentReceived(trade.getTradeId()); + sleep(3_000); + + trade = bobClient.getTrade(tradeId); + // Warning: trade.getOffer().getState() might be AVAILABLE, not OFFER_FEE_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)); + logTrade(log, testInfo, "Bob's Taker/Buyer View (Payment Received)", bobClient.getTrade(tradeId)); + } 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); + bobClient.keepFunds(tradeId); + 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)); + logTrade(log, testInfo, "Bob's Taker/Buyer View (Done)", bobClient.getTrade(tradeId)); + logBalances(log, testInfo); + } catch (StatusRuntimeException e) { + fail(e); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java b/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java index 46195144dcc..39660d42a31 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 0996d21eef4..2d6eef8cbe9 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java @@ -33,6 +33,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 testCreateBSQSwapOffers() { BsqSwapOfferTest test = new BsqSwapOfferTest(); test.testAliceCreateBsqSwapBuyOffer1(); @@ -101,7 +115,7 @@ public void testCreateBSQSwapOffers() { } @Test - @Order(7) + @Order(8) public void testEditOffer() { EditOfferTest test = new EditOfferTest(); // Edit fiat offer tests @@ -124,5 +138,14 @@ 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(); + // Edit bsq swap offer tests + test.testEditBsqSwapOfferShouldThrowException(); } } diff --git a/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java index c4e03ffa2f2..69d5ba6dd80 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java @@ -33,8 +33,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 @@ -99,6 +101,28 @@ public void testTakeSellBSQOffer(final TestInfo testInfo) { @Test @Order(6) + public void testTakeBuyXMROffer(final TestInfo testInfo) { + TakeBuyXMROfferTest test = new TakeBuyXMROfferTest(); + TakeBuyXMROfferTest.createXmrPaymentAccounts(); + test.testTakeAlicesSellBTCForXMROffer(testInfo); + test.testBobsConfirmPaymentStarted(testInfo); + test.testAlicesConfirmPaymentReceived(testInfo); + test.testKeepFunds(testInfo); + } + + @Test + @Order(7) + public void testTakeSellXMROffer(final TestInfo testInfo) { + TakeSellXMROfferTest test = new TakeSellXMROfferTest(); + TakeBuyXMROfferTest.createXmrPaymentAccounts(); + test.testTakeAlicesBuyBTCForXMROffer(testInfo); + test.testAlicesConfirmPaymentStarted(testInfo); + test.testBobsConfirmPaymentReceived(testInfo); + test.testAlicesBtcWithdrawalToExternalAddress(testInfo); + } + + @Test + @Order(8) public void testBsqSwapTradeTest(final TestInfo testInfo) { BsqSwapTradeTest test = new BsqSwapTradeTest(); test.testGetBalancesBeforeTrade(); diff --git a/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java b/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java index c4dec5f59ff..63ebca9b54c 100644 --- a/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java +++ b/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java @@ -29,7 +29,7 @@ public static boolean apiDoesSupportCryptoCurrency(String currencyCode) { public static List getSupportedCryptoCurrencies() { final List result = new ArrayList<>(); result.add("BSQ"); - // result.add("XMR"); // TODO Uncomment when XMR support is added. + result.add("XMR"); result.sort(String::compareTo); return result; } diff --git a/cli/src/main/java/bisq/cli/GrpcClient.java b/cli/src/main/java/bisq/cli/GrpcClient.java index 6d73bc4ab51..17eb49e43ca 100644 --- a/cli/src/main/java/bisq/cli/GrpcClient.java +++ b/cli/src/main/java/bisq/cli/GrpcClient.java @@ -298,8 +298,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 getBsqSwapOffersSortedByDate() { @@ -330,10 +330,6 @@ public List getMyCryptoCurrencyOffersSortedByDate(String currencyCode return offersServiceRequest.getMyCryptoCurrencyOffersSortedByDate(currencyCode); } - public List getMyBsqOffersSortedByDate() { - return offersServiceRequest.getMyBsqOffersSortedByDate(); - } - public List getMyBsqSwapBsqOffersSortedByDate() { return offersServiceRequest.getMyBsqSwapOffersSortedByDate(); } diff --git a/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java b/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java index 94db971dba0..a3f3b1c9356 100644 --- a/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java @@ -20,27 +20,28 @@ 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.Boolean.FALSE; +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") .withOptionalArg() .ofType(boolean.class) - .defaultsTo(FALSE); + .defaultsTo(Boolean.FALSE); public CreateCryptoCurrencyPaymentAcctOptionParser(String[] args) { super(args); @@ -59,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 f75ee37db55..a9788a8956a 100644 --- a/cli/src/main/java/bisq/cli/opts/CreateOfferOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/CreateOfferOptionParser.java @@ -36,7 +36,7 @@ public class CreateOfferOptionParser extends AbstractMethodOptionParser implemen 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/EditOfferOptionParser.java b/cli/src/main/java/bisq/cli/opts/EditOfferOptionParser.java index ee994e77786..557a0ff77d4 100644 --- a/cli/src/main/java/bisq/cli/opts/EditOfferOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/EditOfferOptionParser.java @@ -235,6 +235,9 @@ public BigDecimal getMktPriceMarginAsBigDecimal() { } public boolean isUsingMktPriceMargin() { + // We do not have the offer, so we do not really know if isUsingMktPriceMargin + // should be true or false if editType = ACTIVATION_STATE_ONLY. Take care to + // override this value in the daemon in the ACTIVATION_STATE_ONLY case. return !offerEditType.equals(FIXED_PRICE_ONLY) && !offerEditType.equals(FIXED_PRICE_AND_ACTIVATION_STATE); } 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 c3cd9cdc799..7214d8bc0f8 100644 --- a/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java +++ b/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java @@ -35,6 +35,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; @@ -253,7 +254,6 @@ public OfferInfo getMyBsqSwapOffer(String offerId) { .setId(offerId) .build(); return grpcStubs.offersService.getMyBsqSwapOffer(request).getBsqSwapOffer(); - } public OfferInfo getMyOffer(String offerId) { @@ -272,7 +272,7 @@ public List getBsqSwapOffers(String direction) { } public List getOffers(String direction, String currencyCode) { - if (isSupportedCryptoCurrency(currencyCode)) { + if (apiDoesSupportCryptoCurrency(currencyCode)) { return getCryptoCurrencyOffers(direction, currencyCode); } else { var request = GetOffersRequest.newBuilder() @@ -301,17 +301,17 @@ public List getOffersSortedByDate(String direction, String currencyCo return offers.isEmpty() ? offers : sortOffersByDate(offers); } - public List getBsqSwapOffersSortedByDate() { + public List getCryptoCurrencyOffersSortedByDate(String currencyCode) { ArrayList offers = new ArrayList<>(); - offers.addAll(getBsqSwapOffers(BUY.name())); - offers.addAll(getBsqSwapOffers(SELL.name())); + offers.addAll(getCryptoCurrencyOffers(BUY.name(), currencyCode)); + offers.addAll(getCryptoCurrencyOffers(SELL.name(), currencyCode)); return sortOffersByDate(offers); } - public List getBsqOffersSortedByDate() { + public List getBsqSwapOffersSortedByDate() { ArrayList offers = new ArrayList<>(); - offers.addAll(getCryptoCurrencyOffers(BUY.name(), "BSQ")); - offers.addAll(getCryptoCurrencyOffers(SELL.name(), "BSQ")); + offers.addAll(getBsqSwapOffers(BUY.name())); + offers.addAll(getBsqSwapOffers(SELL.name())); return sortOffersByDate(offers); } @@ -323,7 +323,7 @@ public List getMyBsqSwapOffers(String direction) { } public List getMyOffers(String direction, String currencyCode) { - if (isSupportedCryptoCurrency(currencyCode)) { + if (apiDoesSupportCryptoCurrency(currencyCode)) { return getMyCryptoCurrencyOffers(direction, currencyCode); } else { var request = GetMyOffersRequest.newBuilder() @@ -359,13 +359,6 @@ public List getMyCryptoCurrencyOffersSortedByDate(String currencyCode return sortOffersByDate(offers); } - public List getMyBsqOffersSortedByDate() { - ArrayList offers = new ArrayList<>(); - offers.addAll(getMyCryptoCurrencyOffers(BUY.name(), "BSQ")); - offers.addAll(getMyCryptoCurrencyOffers(SELL.name(), "BSQ")); - return sortOffersByDate(offers); - } - public List getMyBsqSwapOffersSortedByDate() { ArrayList offers = new ArrayList<>(); offers.addAll(getMyBsqSwapOffers(BUY.name())); @@ -391,15 +384,4 @@ private OfferCategory getOfferCategory(String offerId, boolean isMyOffer) { .build(); return grpcStubs.offersService.getOfferCategory(request).getOfferCategory(); } - - 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/test/java/bisq/cli/AbstractCliTest.java b/cli/src/test/java/bisq/cli/AbstractCliTest.java index 6e35142d0ac..a0367ed2602 100644 --- a/cli/src/test/java/bisq/cli/AbstractCliTest.java +++ b/cli/src/test/java/bisq/cli/AbstractCliTest.java @@ -1,16 +1,29 @@ package bisq.cli; +import bisq.proto.grpc.OfferInfo; + import joptsimple.OptionParser; import joptsimple.OptionSet; +import java.math.BigDecimal; + import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import lombok.extern.slf4j.Slf4j; import static bisq.cli.opts.OptLabel.OPT_HOST; import static bisq.cli.opts.OptLabel.OPT_PASSWORD; import static bisq.cli.opts.OptLabel.OPT_PORT; +import static java.lang.System.out; +import static java.math.RoundingMode.HALF_UP; +import static java.util.Arrays.stream; import static org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch.Operation.DELETE; import static org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch.Operation.INSERT; @@ -34,6 +47,19 @@ public abstract class AbstractCliTest { static final String[] BASE_ALICE_CLIENT_OPTS = new String[]{PASSWORD_OPT, ALICE_PORT_OPT}; static final String[] BASE_BOB_CLIENT_OPTS = new String[]{PASSWORD_OPT, BOB_PORT_OPT}; + protected final BiFunction> randomMarginBasedPrices = (min, max) -> + IntStream.range(min, max).asDoubleStream() + .boxed() + .map(d -> d / 100) + .map(Object::toString) + .collect(Collectors.toList()); + + protected final BiFunction randomFixedAltcoinPrice = (min, max) -> { + String random = Double.valueOf(ThreadLocalRandom.current().nextDouble(min, max)).toString(); + BigDecimal bd = new BigDecimal(random).setScale(8, HALF_UP); + return bd.toPlainString(); + }; + protected final GrpcClient aliceClient; protected final GrpcClient bobClient; @@ -93,4 +119,150 @@ protected void printOldTbl(String tbl) { protected void printNewTbl(String tbl) { log.info("NEW Console OUT:\n{}", tbl); } + + protected List getMyAltcoinOffers(String currencyCode) { + String[] args = getMyOffersCommand("buy", currencyCode); + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + + args = getMyOffersCommand("sell", currencyCode); + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + + return aliceClient.getMyCryptoCurrencyOffersSortedByDate(currencyCode); + } + + protected String[] getMyOffersCommand(String direction, String currencyCode) { + return new String[]{ + PASSWORD_OPT, + ALICE_PORT_OPT, + "getmyoffers", + "--direction=" + direction, + "--currency-code=" + currencyCode + }; + } + + protected String[] getAvailableOffersCommand(String direction, String currencyCode) { + return new String[]{ + PASSWORD_OPT, + BOB_PORT_OPT, + "getoffers", + "--direction=" + direction, + "--currency-code=" + currencyCode + }; + } + + + protected void editOfferPriceMargin(OfferInfo offer, String priceMargin, boolean enable) { + String[] args = new String[]{ + PASSWORD_OPT, + ALICE_PORT_OPT, + "editoffer", + "--offer-id=" + offer.getId(), + "--market-price-margin=" + priceMargin, + "--enable=" + enable + }; + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + } + + protected void editOfferTriggerPrice(OfferInfo offer, String triggerPrice, boolean enable) { + String[] args = new String[]{ + PASSWORD_OPT, + ALICE_PORT_OPT, + "editoffer", + "--offer-id=" + offer.getId(), + "--trigger-price=" + triggerPrice, + "--enable=" + enable + }; + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + } + + protected void editOfferPriceMarginAndTriggerPrice(OfferInfo offer, + String priceMargin, + String triggerPrice, + boolean enable) { + String[] args = new String[]{ + PASSWORD_OPT, + ALICE_PORT_OPT, + "editoffer", + "--offer-id=" + offer.getId(), + "--market-price-margin=" + priceMargin, + "--trigger-price=" + triggerPrice, + "--enable=" + enable + }; + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + } + + protected void editOfferFixedPrice(OfferInfo offer, String fixedPrice, boolean enable) { + String[] args = new String[]{ + PASSWORD_OPT, + ALICE_PORT_OPT, + "editoffer", + "--offer-id=" + offer.getId(), + "--fixed-price=" + fixedPrice, + "--enable=" + enable + }; + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + } + + protected void disableOffers(List offers) { + out.println("Disable Offers"); + for (OfferInfo offer : offers) { + editOfferEnable(offer, false); + sleep(5); + } + } + + protected void enableOffers(List offers) { + out.println("Enable Offers"); + for (OfferInfo offer : offers) { + editOfferEnable(offer, true); + sleep(5); + } + } + + protected void editOfferEnable(OfferInfo offer, boolean enable) { + String[] args = new String[]{ + PASSWORD_OPT, + ALICE_PORT_OPT, + "editoffer", + "--offer-id=" + offer.getId(), + "--enable=" + enable + }; + out.print(">>>>> bisq-cli "); + stream(args).forEach(a -> out.print(a + " ")); + out.println(); + CliMain.main(args); + out.println("<<<<<"); + } + + protected void sleep(long seconds) { + try { + TimeUnit.SECONDS.sleep(seconds); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } } diff --git a/cli/src/test/java/bisq/cli/CreateOfferSmokeTest.java b/cli/src/test/java/bisq/cli/CreateOfferSmokeTest.java index 0cb717f935e..3b8a459660e 100644 --- a/cli/src/test/java/bisq/cli/CreateOfferSmokeTest.java +++ b/cli/src/test/java/bisq/cli/CreateOfferSmokeTest.java @@ -19,11 +19,12 @@ public class CreateOfferSmokeTest extends AbstractCliTest { public static void main(String[] args) { - createBsqSwapOffer("buy"); - createBsqSwapOffer("sell"); + CreateOfferSmokeTest test = new CreateOfferSmokeTest(); + test.createBsqSwapOffer("buy"); + test.createBsqSwapOffer("sell"); } - private static void createBsqSwapOffer(String direction) { + private void createBsqSwapOffer(String direction) { String[] args = createBsqSwapOfferCommand(direction, "0.01", "0.005", "0.00005"); out.print(">>>>> bisq-cli "); stream(args).forEach(a -> out.print(a + " ")); @@ -31,14 +32,14 @@ private static void createBsqSwapOffer(String direction) { CliMain.main(args); out.println("<<<<<"); - args = getMyOffersCommand(direction); + args = getMyOffersCommand(direction, "bsq"); out.print(">>>>> bisq-cli "); stream(args).forEach(a -> out.print(a + " ")); out.println(); CliMain.main(args); out.println("<<<<<"); - args = getAvailableOffersCommand(direction); + args = getAvailableOffersCommand(direction, "bsq"); out.print(">>>>> bisq-cli "); stream(args).forEach(a -> out.print(a + " ")); out.println(); @@ -46,10 +47,10 @@ private static void createBsqSwapOffer(String direction) { out.println("<<<<<"); } - private static String[] createBsqSwapOfferCommand(String direction, - String amount, - String minAmount, - String fixedPrice) { + private String[] createBsqSwapOfferCommand(String direction, + String amount, + String minAmount, + String fixedPrice) { return new String[]{ PASSWORD_OPT, ALICE_PORT_OPT, @@ -62,24 +63,4 @@ private static String[] createBsqSwapOfferCommand(String direction, "--fixed-price=" + fixedPrice }; } - - private static String[] getMyOffersCommand(String direction) { - return new String[]{ - PASSWORD_OPT, - ALICE_PORT_OPT, - "getmyoffers", - "--direction=" + direction, - "--currency-code=bsq" - }; - } - - private static String[] getAvailableOffersCommand(String direction) { - return new String[]{ - PASSWORD_OPT, - BOB_PORT_OPT, - "getoffers", - "--direction=" + direction, - "--currency-code=bsq" - }; - } } diff --git a/cli/src/test/java/bisq/cli/EditBsqOffersSmokeTest.java b/cli/src/test/java/bisq/cli/EditBsqOffersSmokeTest.java new file mode 100644 index 00000000000..242364540df --- /dev/null +++ b/cli/src/test/java/bisq/cli/EditBsqOffersSmokeTest.java @@ -0,0 +1,41 @@ +package bisq.cli; + +import bisq.proto.grpc.OfferInfo; + +import java.util.List; +import java.util.Random; + +import static java.lang.System.out; + +/** + Smoke tests for the editoffer method. + + Prerequisites: + + - Run `./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdesktop --shutdownAfterTests=false --enableBisqDebugging=false` + + - Create some v1 protocol BSQ offers with Alice's UI or CLI. + + - Watch Alice's offers being edited in Bob's UI. + + Never run on mainnet. + */ +public class EditBsqOffersSmokeTest extends AbstractCliTest { + + public static void main(String[] args) { + EditBsqOffersSmokeTest test = new EditBsqOffersSmokeTest(); + + var myBsqOffers = test.getMyAltcoinOffers("bsq"); + test.doOfferPriceEdits(myBsqOffers); + test.disableOffers(myBsqOffers); + } + + private void doOfferPriceEdits(List offers) { + out.println("Edit BSQ offers' fixed price"); + for (int i = 0; i < offers.size(); i++) { + String randomFixedPrice = randomFixedAltcoinPrice.apply(0.000035, 0.00006); + editOfferFixedPrice(offers.get(i), randomFixedPrice, new Random().nextBoolean()); + sleep(5); + } + } +} diff --git a/cli/src/test/java/bisq/cli/EditXmrOffersSmokeTest.java b/cli/src/test/java/bisq/cli/EditXmrOffersSmokeTest.java new file mode 100644 index 00000000000..afcc1446cf7 --- /dev/null +++ b/cli/src/test/java/bisq/cli/EditXmrOffersSmokeTest.java @@ -0,0 +1,100 @@ +package bisq.cli; + +import bisq.proto.grpc.OfferInfo; + +import java.util.List; +import java.util.Random; + +import static java.lang.System.out; +import static protobuf.OfferDirection.BUY; + +/** + Smoke tests for the editoffer method. + + Prerequisites: + + - Run `./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdesktop --shutdownAfterTests=false --enableBisqDebugging=false` + + - Create some XMR offers with Alice's UI or CLI. + + - Watch Alice's offers being edited in Bob's UI. + + Never run on mainnet. + */ +public class EditXmrOffersSmokeTest extends AbstractCliTest { + + public static void main(String[] args) { + var test = new EditXmrOffersSmokeTest(); + + test.doOfferPriceEdits(); + + List offers = test.getMyAltcoinOffers("xmr"); + test.disableOffers(offers); + + test.sleep(6); + + offers = test.getMyAltcoinOffers("xmr"); + test.enableOffers(offers); + + // A final look after last edit. + test.getMyAltcoinOffers("xmr"); + } + + private void doOfferPriceEdits() { + editPriceMargin(); + editTriggerPrice(); + editPriceMarginAndTriggerPrice(); + editFixedPrice(); + } + + private void editPriceMargin() { + var offers = getMyAltcoinOffers("xmr"); + out.println("Edit XMR offers' price margin"); + var margins = randomMarginBasedPrices.apply(-301, 300); + for (int i = 0; i < offers.size(); i++) { + String randomMargin = margins.get(new Random().nextInt(margins.size())); + editOfferPriceMargin(offers.get(i), randomMargin, new Random().nextBoolean()); + sleep(5); + } + } + + private void editTriggerPrice() { + var offers = getMyAltcoinOffers("xmr"); + out.println("Edit XMR offers' trigger price"); + for (int i = 0; i < offers.size(); i++) { + var offer = offers.get(i); + if (offer.getUseMarketBasedPrice()) { + // Trigger price is hardcode to be a bit above or below xmr mkt price at runtime. + // It could be looked up and calculated instead. + var newTriggerPrice = offer.getDirection().equals(BUY.name()) ? "0.0039" : "0.005"; + editOfferTriggerPrice(offer, newTriggerPrice, true); + sleep(5); + } + } + } + + private void editPriceMarginAndTriggerPrice() { + var offers = getMyAltcoinOffers("xmr"); + out.println("Edit XMR offers' price margin and trigger price"); + for (int i = 0; i < offers.size(); i++) { + var offer = offers.get(i); + if (offer.getUseMarketBasedPrice()) { + // Trigger price is hardcode to be a bit above or below xmr mkt price at runtime. + // It could be looked up and calculated instead. + var newTriggerPrice = offer.getDirection().equals(BUY.name()) ? "0.0038" : "0.0051"; + editOfferPriceMarginAndTriggerPrice(offer, "0.05", newTriggerPrice, true); + sleep(5); + } + } + } + + private void editFixedPrice() { + var offers = getMyAltcoinOffers("xmr"); + out.println("Edit XMR offers' fixed price"); + for (int i = 0; i < offers.size(); i++) { + String randomFixedPrice = randomFixedAltcoinPrice.apply(0.004, 0.0075); + editOfferFixedPrice(offers.get(i), randomFixedPrice, new Random().nextBoolean()); + sleep(5); + } + } +} diff --git a/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java b/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java index fc6ee7b4dbd..538587a3dee 100644 --- a/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java +++ b/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java @@ -39,7 +39,6 @@ private static void getAvailableBsqOffers() { CliMain.main(new String[]{"--password=xyz", "--port=9998", "getoffers", "--direction=sell", "--currency-code=bsq"}); } - private static void getMyUsdOffers() { out.println(">>> getmyoffers buy usd"); CliMain.main(new String[]{"--password=xyz", "--port=9998", "getmyoffers", "--direction=buy", "--currency-code=usd"}); diff --git a/cli/src/test/java/bisq/cli/opts/OptionParsersTest.java b/cli/src/test/java/bisq/cli/opts/OptionParsersTest.java index ba527a88e5a..cd8062cdd10 100644 --- a/cli/src/test/java/bisq/cli/opts/OptionParsersTest.java +++ b/cli/src/test/java/bisq/cli/opts/OptionParsersTest.java @@ -327,12 +327,12 @@ public void testCreateCryptoCurrencyPaymentAcctWithInvalidCurrencyCodeOptShouldT 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 @@ -349,7 +349,7 @@ public void testCreateCryptoCurrencyPaymentAcctWithMissingAddressOptShouldThrowE } @Test - public void testCreateCryptoCurrencyPaymentAcct() { + public void testCreateV1BsqPaymentAcct() { var acctName = "bsq payment account"; var currencyCode = "bsq"; var address = "B1nXyZ"; // address is validated on server @@ -365,4 +365,22 @@ public void testCreateCryptoCurrencyPaymentAcct() { assertEquals(currencyCode, parser.getCurrencyCode()); assertEquals(address, parser.getAddress()); } + + @Test + public void testCreateV1XmrPaymentAcct() { + var acctName = "xmr payment account"; + var currencyCode = "xmr"; + var address = "B1nXyZ46XXX"; // address is validated on server + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name(), + "--" + OPT_ACCOUNT_NAME + "=" + acctName, + "--" + OPT_CURRENCY_CODE + "=" + currencyCode, + "--" + OPT_ADDRESS + "=" + address + }; + var parser = new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse(); + assertEquals(acctName, parser.getAccountName()); + assertEquals(currencyCode, parser.getCurrencyCode()); + assertEquals(address, parser.getAddress()); + } } diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index caa9e2650a6..137f81c74c2 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -226,8 +226,8 @@ public void cancelOffer(String id) { coreOffersService.cancelOffer(id); } - public boolean isMyOffer(String id) { - return coreOffersService.isMyOffer(id); + public boolean isMyOffer(Offer offer) { + return coreOffersService.isMyOffer(offer); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index fde91f273c6..ce6219d20c2 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -67,7 +67,8 @@ import static bisq.core.offer.OpenOffer.State.DEACTIVATED; import static bisq.core.payment.PaymentAccountUtil.isPaymentAccountValidForOffer; import static bisq.proto.grpc.EditOfferRequest.EditType; -import static bisq.proto.grpc.EditOfferRequest.EditType.*; +import static bisq.proto.grpc.EditOfferRequest.EditType.FIXED_PRICE_AND_ACTIVATION_STATE; +import static bisq.proto.grpc.EditOfferRequest.EditType.FIXED_PRICE_ONLY; import static java.lang.String.format; import static java.util.Comparator.comparing; @@ -209,13 +210,8 @@ OpenOffer getMyOpenOffer(String id) { new IllegalStateException(format("offer with id '%s' not found", id))); } - boolean isMyOffer(String id) { - boolean isMyOpenOffer = openOfferManager.getOpenOfferById(id) - .filter(open -> open.getOffer().isMyOffer(keyRing)) - .isPresent(); - boolean wasMyOffer = offerBookService.getOffers().stream() - .anyMatch(o -> o.getId().equals(id) && o.isMyOffer(keyRing)); - return isMyOpenOffer || wasMyOffer; + boolean isMyOffer(Offer offer) { + return offer.isMyOffer(keyRing); } void createAndPlaceBsqSwapOffer(String directionAsString, @@ -300,27 +296,14 @@ void editOffer(String offerId, int editedEnable, EditType editType) { OpenOffer openOffer = getMyOpenOffer(offerId); - new EditOfferValidator(openOffer, + var validator = new EditOfferValidator(openOffer, editedPriceAsString, editedUseMarketBasedPrice, editedMarketPriceMargin, editedTriggerPrice, editedEnable, editType).validate(); - log.info("Validated 'editoffer' params offerId={}" - + "\n\teditedPriceAsString={}" - + "\n\teditedUseMarketBasedPrice={}" - + "\n\teditedMarketPriceMargin={}" - + "\n\teditedTriggerPrice={}" - + "\n\teditedEnable={}" - + "\n\teditType={}", - offerId, - editedPriceAsString, - editedUseMarketBasedPrice, - editedMarketPriceMargin, - editedTriggerPrice, - editedEnable, - editType); + log.info(validator.toString()); OpenOffer.State currentOfferState = openOffer.getState(); // Client sent (sint32) editedEnable, not a bool (with default=false). // If editedEnable = -1, do not change current state @@ -329,7 +312,8 @@ void editOffer(String offerId, OpenOffer.State newOfferState = editedEnable < 0 ? currentOfferState : editedEnable > 0 ? AVAILABLE : DEACTIVATED; - OfferPayload editedPayload = getMergedOfferPayload(openOffer, + OfferPayload editedPayload = getMergedOfferPayload(validator, + openOffer, editedPriceAsString, editedMarketPriceMargin, editType); @@ -412,7 +396,8 @@ private Optional findMyOpenOffer(String id) { .findAny(); } - private OfferPayload getMergedOfferPayload(OpenOffer openOffer, + private OfferPayload getMergedOfferPayload(EditOfferValidator editOfferValidator, + OpenOffer openOffer, String editedPriceAsString, double editedMarketPriceMargin, EditType editType) { @@ -429,15 +414,16 @@ private OfferPayload getMergedOfferPayload(OpenOffer openOffer, } else { editedPrice = offer.getPrice(); } - boolean isUsingMktPriceMargin = editType.equals(MKT_PRICE_MARGIN_ONLY) - || editType.equals(MKT_PRICE_MARGIN_AND_ACTIVATION_STATE) - || editType.equals(TRIGGER_PRICE_ONLY) - || editType.equals(TRIGGER_PRICE_AND_ACTIVATION_STATE) - || editType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE) - || editType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE); + + boolean isUsingMktPriceMargin = editOfferValidator.isEditingUseMktPriceMarginFlag.test(offer, editType); + boolean isEditingMktPriceMargin = editOfferValidator.isEditingMktPriceMargin.test(editType); + double newMarketPriceMargin = isEditingMktPriceMargin + ? exactMultiply(editedMarketPriceMargin, 0.01) + : offer.getMarketPriceMargin(); + MutableOfferPayloadFields mutableOfferPayloadFields = new MutableOfferPayloadFields( Objects.requireNonNull(editedPrice).getValue(), - isUsingMktPriceMargin ? exactMultiply(editedMarketPriceMargin, 0.01) : 0.00, + isUsingMktPriceMargin ? newMarketPriceMargin : 0.00, isUsingMktPriceMargin, offer.getBaseCurrencyCode(), offer.getCounterCurrencyCode(), diff --git a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java index 0638ddbda4c..de053847384 100644 --- a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java @@ -28,6 +28,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; @@ -37,10 +40,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; @@ -48,6 +55,9 @@ @Slf4j class CorePaymentAccountsService { + private final Predicate apiDoesSupportCryptoCurrencyAccount = (c) -> + c.equals("BSQ") || c.equals("XMR"); + private final CoreWalletsService coreWalletsService; private final AccountAgeWitnessService accountAgeWitnessService; private final PaymentAccountForm paymentAccountForm; @@ -104,12 +114,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); AssetAccount cryptoCurrencyAccount = tradeInstant ? (InstantCryptoCurrencyAccount) PaymentAccountFactory.getPaymentAccount(PaymentMethod.BLOCK_CHAINS_INSTANT) @@ -117,7 +124,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); log.info("Saved crypto payment account with id {} and payment method {}.", @@ -137,6 +144,38 @@ 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 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/EditOfferValidator.java b/core/src/main/java/bisq/core/api/EditOfferValidator.java index 89c9366e1eb..34515da5548 100644 --- a/core/src/main/java/bisq/core/api/EditOfferValidator.java +++ b/core/src/main/java/bisq/core/api/EditOfferValidator.java @@ -1,50 +1,91 @@ package bisq.core.api; +import bisq.core.offer.Offer; import bisq.core.offer.OpenOffer; import bisq.proto.grpc.EditOfferRequest; import java.math.BigDecimal; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + import lombok.extern.slf4j.Slf4j; -import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; +import static bisq.proto.grpc.EditOfferRequest.EditType.*; import static java.lang.String.format; @Slf4j class EditOfferValidator { + public final BiPredicate isEditingUseMktPriceMarginFlag = (offer, editType) -> { + if (editType.equals(ACTIVATION_STATE_ONLY)) { + // If only changing activation state, we are not editing offer.isUseMarketBasedPrice flag. + return offer.isUseMarketBasedPrice(); + } else { + return editType.equals(MKT_PRICE_MARGIN_ONLY) + || editType.equals(MKT_PRICE_MARGIN_AND_ACTIVATION_STATE) + || editType.equals(TRIGGER_PRICE_ONLY) + || editType.equals(TRIGGER_PRICE_AND_ACTIVATION_STATE) + || editType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE) + || editType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE); + } + }; + + public final Predicate isEditingMktPriceMargin = (editType) -> + editType.equals(MKT_PRICE_MARGIN_ONLY) + || editType.equals(MKT_PRICE_MARGIN_AND_ACTIVATION_STATE) + || editType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE) + || editType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE); + + public final Predicate isEditingTriggerPrice = (editType) -> + editType.equals(TRIGGER_PRICE_ONLY) + || editType.equals(TRIGGER_PRICE_AND_ACTIVATION_STATE) + || editType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE) + || editType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE); + + public final Predicate isEditingFixedPrice = (editType) -> + editType.equals(FIXED_PRICE_ONLY) || editType.equals(FIXED_PRICE_AND_ACTIVATION_STATE); + + private final OpenOffer currentlyOpenOffer; - private final String editedPriceAsString; - private final boolean editedUseMarketBasedPrice; - private final double editedMarketPriceMargin; - private final long editedTriggerPrice; - private final int editedEnable; + private final String newPriceAsString; + private final boolean newIsUseMarketBasedPrice; + private final double newMarketPriceMargin; + private final long newTriggerPrice; + private final int newEnable; private final EditOfferRequest.EditType editType; private final boolean isZeroEditedFixedPriceString; private final boolean isZeroEditedTriggerPrice; EditOfferValidator(OpenOffer currentlyOpenOffer, - String editedPriceAsString, - boolean editedUseMarketBasedPrice, - double editedMarketPriceMargin, - long editedTriggerPrice, - int editedEnable, + String newPriceAsString, + boolean newIsUseMarketBasedPrice, + double newMarketPriceMargin, + long newTriggerPrice, + int newEnable, EditOfferRequest.EditType editType) { this.currentlyOpenOffer = currentlyOpenOffer; - this.editedPriceAsString = editedPriceAsString; - this.editedUseMarketBasedPrice = editedUseMarketBasedPrice; - this.editedMarketPriceMargin = editedMarketPriceMargin; - this.editedTriggerPrice = editedTriggerPrice; - this.editedEnable = editedEnable; + this.newPriceAsString = newPriceAsString; + // The client cannot determine what offer.isUseMarketBasedPrice should be + // when editType = ACTIVATION_STATE_ONLY. Override newIsUseMarketBasedPrice + // param for the ACTIVATION_STATE_ONLY case. + // A cleaner solution might be possible if the client fetched the offer + // before sending an edit request, but that's an extra round trip to the server. + this.newIsUseMarketBasedPrice = editType.equals(ACTIVATION_STATE_ONLY) + ? currentlyOpenOffer.getOffer().isUseMarketBasedPrice() + : newIsUseMarketBasedPrice; + this.newMarketPriceMargin = newMarketPriceMargin; + this.newTriggerPrice = newTriggerPrice; + this.newEnable = newEnable; this.editType = editType; - this.isZeroEditedFixedPriceString = new BigDecimal(editedPriceAsString).doubleValue() == 0; - this.isZeroEditedTriggerPrice = editedTriggerPrice == 0; + this.isZeroEditedFixedPriceString = new BigDecimal(newPriceAsString).doubleValue() == 0; + this.isZeroEditedTriggerPrice = newTriggerPrice == 0; } - void validate() { + EditOfferValidator validate() { log.info("Verifying 'editoffer' params for editType {}", editType); checkNotBsqSwapOffer(); switch (editType) { @@ -63,7 +104,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; @@ -71,14 +112,44 @@ void validate() { default: break; } + return this; + } + + @Override + public String toString() { + boolean isEditingMktPriceMargin = this.isEditingMktPriceMargin.test(editType); + boolean isEditingPrice = isEditingFixedPrice.test(editType); + var offer = currentlyOpenOffer.getOffer(); + return "EditOfferValidator{" + "\n" + + " offer=" + offer.getId() + "\n" + + ", offer.payloadBase.price=" + offer.getOfferPayloadBase().getPrice() + "\n" + + ", newPriceAsString=" + (isEditingPrice ? newPriceAsString : "N/A") + "\n" + + ", offer.useMarketBasedPrice=" + offer.isUseMarketBasedPrice() + "\n" + + ", newUseMarketBasedPrice=" + newIsUseMarketBasedPrice + "\n" + + ", offer.marketPriceMargin=" + offer.getMarketPriceMargin() + "\n" + + ", newMarketPriceMargin=" + (isEditingMktPriceMargin ? newMarketPriceMargin : "N/A") + "\n" + + ", offer.triggerPrice=" + currentlyOpenOffer.getTriggerPrice() + "\n" + + ", newTriggerPrice=" + (isEditingTriggerPrice.test(editType) ? newTriggerPrice : "N/A") + "\n" + + ", newEnable=" + newEnable + "\n" + + ", editType=" + editType + "\n" + + '}'; } private void validateEditedActivationState() { - if (editedEnable < 0) + if (newEnable < 0) throw new IllegalStateException( format("programmer error: the 'enable' request parameter does not" + " indicate activation state of offer with id '%s' should be changed.", currentlyOpenOffer.getId())); + + var enableDescription = newEnable == 0 ? "deactivate" : "activate"; + var pricingDescription = currentlyOpenOffer.getOffer().isUseMarketBasedPrice() + ? "mkt price margin" + : "fixed price"; + log.info("Attempting to {} {} offer with id '{}'.", + enableDescription, + pricingDescription, + currentlyOpenOffer.getId()); } private void validateEditedFixedPrice() { @@ -86,18 +157,18 @@ private void validateEditedFixedPrice() { log.info("Attempting to change mkt price margin based offer with id '{}' to fixed price offer.", currentlyOpenOffer.getId()); - if (editedUseMarketBasedPrice) + if (newIsUseMarketBasedPrice) throw new IllegalStateException( format("programmer error: cannot change fixed price (%s)" + " in mkt price based offer with id '%s'", - editedMarketPriceMargin, + newMarketPriceMargin, currentlyOpenOffer.getId())); if (!isZeroEditedTriggerPrice) throw new IllegalStateException( format("programmer error: cannot change trigger price (%s)" + " in offer with id '%s' when changing fixed price", - editedTriggerPrice, + newTriggerPrice, currentlyOpenOffer.getId())); } @@ -111,31 +182,30 @@ private void validateEditedMarketPriceMargin() { throw new IllegalStateException( format("programmer error: cannot set fixed price (%s)" + " in mkt price margin based offer with id '%s'", - editedPriceAsString, + newPriceAsString, currentlyOpenOffer.getId())); } private void validateEditedTriggerPrice() { if (!currentlyOpenOffer.getOffer().isUseMarketBasedPrice() - && !editedUseMarketBasedPrice + && !newIsUseMarketBasedPrice && !isZeroEditedTriggerPrice) throw new IllegalStateException( - format("programmer error: cannot set a trigger price (%s)" + format("programmer error: cannot set a trigger price" + " in fixed price offer with id '%s'", - editedTriggerPrice, currentlyOpenOffer.getId())); - if (editedTriggerPrice < 0) + if (newTriggerPrice < 0) throw new IllegalStateException( format("programmer error: cannot set trigger price to a negative value" + " in offer with id '%s'", 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/api/model/OfferInfo.java b/core/src/main/java/bisq/core/api/model/OfferInfo.java index 6cf635cb3b2..7e4c8c49b13 100644 --- a/core/src/main/java/bisq/core/api/model/OfferInfo.java +++ b/core/src/main/java/bisq/core/api/model/OfferInfo.java @@ -168,9 +168,14 @@ private static OfferInfoBuilder getBuilder(Offer offer, boolean isMyOffer) { } private static long getMakerFee(Offer offer, boolean isMyOffer) { - return isMyOffer - ? requireNonNull(CoinUtil.getMakerFee(false, offer.getAmount())).value - : 0; + // TODO Find out why offer.makerFee is always set to 0 when offer is bsq-swap. + if (isMyOffer) { + return offer.isBsqSwapOffer() + ? requireNonNull(CoinUtil.getMakerFee(false, offer.getAmount())).value + : offer.getMakerFee().value; + } else { + return 0; + } } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/offer/bisq_v1/MutableOfferPayloadFields.java b/core/src/main/java/bisq/core/offer/bisq_v1/MutableOfferPayloadFields.java index b2025267540..e0974da047f 100644 --- a/core/src/main/java/bisq/core/offer/bisq_v1/MutableOfferPayloadFields.java +++ b/core/src/main/java/bisq/core/offer/bisq_v1/MutableOfferPayloadFields.java @@ -22,7 +22,6 @@ import lombok.Getter; import lombok.Setter; -import lombok.ToString; import javax.annotation.Nullable; @@ -31,7 +30,6 @@ */ @Getter @Setter -@ToString public final class MutableOfferPayloadFields { private final long price; @@ -92,4 +90,22 @@ public MutableOfferPayloadFields(long price, this.acceptedBankIds = acceptedBankIds; this.extraDataMap = extraDataMap; } + + @Override + public String toString() { + return "MutableOfferPayloadFields{" + "\n" + + " price=" + price + "\n" + + ", marketPriceMargin=" + marketPriceMargin + "\n" + + ", useMarketBasedPrice=" + useMarketBasedPrice + "\n" + + ", baseCurrencyCode='" + baseCurrencyCode + '\'' + "\n" + + ", counterCurrencyCode='" + counterCurrencyCode + '\'' + "\n" + + ", paymentMethodId='" + paymentMethodId + '\'' + "\n" + + ", makerPaymentAccountId='" + makerPaymentAccountId + '\'' + "\n" + + ", countryCode='" + countryCode + '\'' + "\n" + + ", acceptedCountryCodes=" + acceptedCountryCodes + "\n" + + ", bankId='" + bankId + '\'' + "\n" + + ", acceptedBankIds=" + acceptedBankIds + "\n" + + ", extraDataMap=" + extraDataMap + "\n" + + '}'; + } } diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java index 6a9ce12a7cb..4d2605b109a 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -234,7 +234,7 @@ private GetTradeReply buildGetTradeReply(Trade trade) { } private boolean wasMyOffer(TradeModel tradeModel) { - return coreApi.isMyOffer(tradeModel.getOffer().getId()); + return coreApi.isMyOffer(tradeModel.getOffer()); } private String getMyRole(TradeModel tradeModel) {