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 addd0733453..57495bb696a 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java @@ -17,12 +17,15 @@ package bisq.apitest.method.offer; +import bisq.core.offer.OfferDirection; + import bisq.proto.grpc.OfferInfo; import protobuf.PaymentAccount; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.List; import java.util.function.BiFunction; import java.util.function.Function; @@ -42,12 +45,16 @@ import static bisq.apitest.config.BisqAppConfig.seednode; import static bisq.cli.table.builder.TableType.OFFER_TBL; import static bisq.common.util.MathUtils.exactMultiply; +import static java.lang.String.format; import static java.lang.System.out; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import bisq.apitest.method.MethodTest; import bisq.cli.CliMain; +import bisq.cli.GrpcClient; import bisq.cli.table.builder.TableBuilder; @Slf4j @@ -122,6 +129,34 @@ public static void setUp() { protected final Function, String> toOffersTable = (offers) -> new TableBuilder(OFFER_TBL, offers).build().toString(); + protected OfferInfo getAvailableBsqSwapOffer(GrpcClient client, + OfferDirection direction, + boolean checkForLoggedExceptions) { + List bsqSwapOffers = new ArrayList<>(); + int numFetchAttempts = 0; + while (bsqSwapOffers.size() == 0) { + bsqSwapOffers.addAll(client.getBsqSwapOffers(direction.name())); + numFetchAttempts++; + if (bsqSwapOffers.size() == 0) { + log.warn("No available bsq swap offers found after {} fetch attempts.", numFetchAttempts); + if (numFetchAttempts > 9) { + if (checkForLoggedExceptions) { + printNodeExceptionMessages(log); + } + fail(format("Bob gave up on fetching available bsq swap offers after %d attempts.", numFetchAttempts)); + } + sleep(1_000); + } else { + assertEquals(1, bsqSwapOffers.size()); + log.debug("Bob found new available bsq swap offer on attempt # {}.", numFetchAttempts); + break; + } + } + var bsqSwapOffer = bobClient.getBsqSwapOffer(bsqSwapOffers.get(0).getId()); + assertEquals(bsqSwapOffers.get(0).getId(), bsqSwapOffer.getId()); + return bsqSwapOffer; + } + @SuppressWarnings("ConstantConditions") public static void initSwapPaymentAccounts() { // A bot may not know what the default 'BSQ Swap' account name is, 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 2c1fb29d320..5ceebe026a2 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java @@ -264,4 +264,18 @@ protected static void runCliGetTrade(String tradeId) { out.println("Bob's CLI 'gettrade' response:"); CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrade", "--trade-id=" + tradeId}); } + + protected static void runCliGetOpenTrades() { + out.println("Alice's CLI 'gettrades --category=open' response:"); + CliMain.main(new String[]{"--password=xyz", "--port=9998", "gettrades", "--category=open"}); + out.println("Bob's CLI 'gettrades --category=open' response:"); + CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrades", "--category=open"}); + } + + protected static void runCliGetClosedTrades() { + out.println("Alice's CLI 'gettrades --category=closed' response:"); + CliMain.main(new String[]{"--password=xyz", "--port=9998", "gettrades", "--category=closed"}); + out.println("Bob's CLI 'gettrades --category=closed' response:"); + CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrades", "--category=closed"}); + } } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/BsqSwapTradeTest.java b/apitest/src/test/java/bisq/apitest/method/trade/BsqSwapBuyBtcTradeTest.java similarity index 80% rename from apitest/src/test/java/bisq/apitest/method/trade/BsqSwapTradeTest.java rename to apitest/src/test/java/bisq/apitest/method/trade/BsqSwapBuyBtcTradeTest.java index bbf2074c7be..cfa330e7478 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/BsqSwapTradeTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/BsqSwapBuyBtcTradeTest.java @@ -17,11 +17,9 @@ package bisq.apitest.method.trade; -import bisq.proto.grpc.OfferInfo; import bisq.proto.grpc.TradeInfo; -import java.util.ArrayList; -import java.util.List; +import protobuf.OfferDirection; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -36,6 +34,7 @@ import static bisq.apitest.config.ApiTestConfig.BSQ; import static bisq.apitest.config.ApiTestConfig.BTC; +import static bisq.core.offer.OfferDirection.BUY; import static bisq.proto.grpc.GetOfferCategoryReply.OfferCategory.BSQ_SWAP; import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -44,7 +43,6 @@ import static org.junit.jupiter.api.Assertions.fail; import static protobuf.BsqSwapTrade.State.COMPLETED; import static protobuf.BsqSwapTrade.State.PREPARATION; -import static protobuf.OfferDirection.BUY; @@ -54,7 +52,7 @@ @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class BsqSwapTradeTest extends AbstractTradeTest { +public class BsqSwapBuyBtcTradeTest extends AbstractTradeTest { // Long-running swap trade tests might want to check node logs for exceptions. @Setter @@ -81,15 +79,16 @@ public void testGetBalancesBeforeTrade() { @Test @Order(2) - public void testAliceCreateBsqSwapBuyOffer() { - var mySwapOffer = aliceClient.createBsqSwapOffer(BUY.name(), + public void testAliceCreateBsqSwapBuyBtcOffer() { + // Alice buys BTC, pays trade fee. Bob (BTC seller) pays miner tx fee. + var mySwapOffer = aliceClient.createBsqSwapOffer(OfferDirection.BUY.name(), 1_000_000L, // 0.01 BTC 1_000_000L, "0.00005"); log.debug("Pending BsqSwap Sell BSQ (Buy BTC) OFFER:\n{}", toOfferTable.apply(mySwapOffer)); var newOfferId = mySwapOffer.getId(); assertNotEquals("", newOfferId); - assertEquals(BUY.name(), mySwapOffer.getDirection()); + assertEquals(OfferDirection.BUY.name(), mySwapOffer.getDirection()); assertEquals(5_000, mySwapOffer.getPrice()); assertEquals(1_000_000L, mySwapOffer.getAmount()); assertEquals(1_000_000L, mySwapOffer.getMinAmount()); @@ -108,7 +107,7 @@ public void testAliceCreateBsqSwapBuyOffer() { @Test @Order(3) public void testBobTakesBsqSwapOffer() { - var availableSwapOffer = getAvailableBsqSwapOffer(bobClient); + var availableSwapOffer = getAvailableBsqSwapOffer(bobClient, BUY, true); // Before sending a TakeOfferRequest, the CLI needs to know what kind of Offer // it is taking (v1 or BsqSwap). Only BSQ swap offers can be taken with a @@ -118,7 +117,7 @@ public void testBobTakesBsqSwapOffer() { var availableOfferCategory = bobClient.getAvailableOfferCategory(availableSwapOffer.getId()); assertTrue(availableOfferCategory.equals(BSQ_SWAP)); - sleep(30_000); + sleep(3_000); var swapTrade = bobClient.takeBsqSwapOffer(availableSwapOffer.getId()); tradeId = swapTrade.getTradeId(); // Cache the tradeId for following test case(s). @@ -130,7 +129,7 @@ public void testBobTakesBsqSwapOffer() { log.debug("BsqSwap Trade at COMPLETION:\n{}", toTradeDetailTable.apply(swapTrade)); assertEquals(COMPLETED.name(), swapTrade.getState()); - runCliGetTrade(tradeId); + runCliGetClosedTrades(); } @Test @@ -151,6 +150,8 @@ public void testCompletedSwapTxConfirmations() { bobsTrade = getBsqSwapTrade(bobClient, tradeId); log.debug("Bob's BsqSwap Trade at COMPLETION:\n{}", toTradeDetailTable.apply(bobsTrade)); assertEquals(2, bobsTrade.getBsqSwapTradeInfo().getNumConfirmations()); + + runCliGetClosedTrades(); } @Test @@ -162,32 +163,6 @@ public void testGetBalancesAfterTrade() { log.debug("Bob's After Trade Balance:\n{}", formatBalancesTbls(bobsBalances)); } - private OfferInfo getAvailableBsqSwapOffer(GrpcClient client) { - List bsqSwapOffers = new ArrayList<>(); - int numFetchAttempts = 0; - while (bsqSwapOffers.size() == 0) { - bsqSwapOffers.addAll(client.getBsqSwapOffers(BUY.name())); - numFetchAttempts++; - if (bsqSwapOffers.size() == 0) { - log.warn("No available bsq swap offers found after {} fetch attempts.", numFetchAttempts); - if (numFetchAttempts > 9) { - if (checkForLoggedExceptions) { - printNodeExceptionMessages(log); - } - fail(format("Bob gave up on fetching available bsq swap offers after %d attempts.", numFetchAttempts)); - } - sleep(1_000); - } else { - assertEquals(1, bsqSwapOffers.size()); - log.debug("Bob found new available bsq swap offer on attempt # {}.", numFetchAttempts); - break; - } - } - var bsqSwapOffer = bobClient.getBsqSwapOffer(bsqSwapOffers.get(0).getId()); - assertEquals(bsqSwapOffers.get(0).getId(), bsqSwapOffer.getId()); - return bsqSwapOffer; - } - private TradeInfo getBsqSwapTrade(GrpcClient client, String tradeId) { int numFetchAttempts = 0; while (true) { diff --git a/apitest/src/test/java/bisq/apitest/method/trade/BsqSwapSellBtcTradeTest.java b/apitest/src/test/java/bisq/apitest/method/trade/BsqSwapSellBtcTradeTest.java new file mode 100644 index 00000000000..80cd48ec1f0 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/BsqSwapSellBtcTradeTest.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 bisq.proto.grpc.TradeInfo; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +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.core.offer.OfferDirection.SELL; +import static bisq.proto.grpc.GetOfferCategoryReply.OfferCategory.BSQ_SWAP; +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static protobuf.BsqSwapTrade.State.COMPLETED; +import static protobuf.BsqSwapTrade.State.PREPARATION; + + + +import bisq.apitest.method.offer.AbstractOfferTest; +import bisq.cli.GrpcClient; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class BsqSwapSellBtcTradeTest extends AbstractTradeTest { + + // Long-running swap trade tests might want to check node logs for exceptions. + @Setter + private boolean checkForLoggedExceptions; + + @BeforeAll + public static void setUp() { + AbstractOfferTest.setUp(); + } + + @BeforeEach + public void generateBtcBlock() { + genBtcBlocksThenWait(1, 2_000); + } + + @Test + @Order(1) + public void testGetBalancesBeforeTrade() { + var alicesBalances = aliceClient.getBalances(); + log.debug("Alice's Before Trade Balance:\n{}", formatBalancesTbls(alicesBalances)); + var bobsBalances = bobClient.getBalances(); + log.debug("Bob's Before Trade Balance:\n{}", formatBalancesTbls(bobsBalances)); + } + + @Test + @Order(2) + public void testAliceCreateBsqSwapSellBtcOffer() { + // Alice sells BTC, pays miner tx fee. Bob (BTC buyer) pays trade fee. + var mySwapOffer = aliceClient.createBsqSwapOffer(SELL.name(), + 1_000_000L, // 0.01 BTC + 1_000_000L, + "0.00005"); + log.debug("Pending BsqSwap Buy BSQ (Sell BTC) OFFER:\n{}", toOfferTable.apply(mySwapOffer)); + var newOfferId = mySwapOffer.getId(); + assertNotEquals("", newOfferId); + assertEquals(SELL.name(), mySwapOffer.getDirection()); + assertEquals(5_000, mySwapOffer.getPrice()); + assertEquals(1_000_000L, mySwapOffer.getAmount()); + assertEquals(1_000_000L, mySwapOffer.getMinAmount()); + assertEquals(BSQ, mySwapOffer.getBaseCurrencyCode()); + assertEquals(BTC, mySwapOffer.getCounterCurrencyCode()); + + genBtcBlocksThenWait(1, 2_500); + + mySwapOffer = aliceClient.getOffer(newOfferId); + log.debug("My fetched BsqSwap Buy BSQ (Sell BTC) OFFER:\n{}", toOfferTable.apply(mySwapOffer)); + assertNotEquals(0, mySwapOffer.getMakerFee()); + + runCliGetOffer(newOfferId); + } + + @Test + @Order(3) + public void testBobTakesBsqSwapOffer() { + var availableSwapOffer = getAvailableBsqSwapOffer(bobClient, SELL, true); + + // Before sending a TakeOfferRequest, the CLI needs to know what kind of Offer + // it is taking (v1 or BsqSwap). Only BSQ swap offers can be taken with a + // single offerId parameter. Taking v1 offers requires an additional + // paymentAccountId param. The test case knows what kind of offer is being taken, + // but we test the gRPC GetOfferCategory service here. + var availableOfferCategory = bobClient.getAvailableOfferCategory(availableSwapOffer.getId()); + assertTrue(availableOfferCategory.equals(BSQ_SWAP)); + + sleep(10_000); + + var swapTrade = bobClient.takeBsqSwapOffer(availableSwapOffer.getId()); + tradeId = swapTrade.getTradeId(); // Cache the tradeId for following test case(s). + log.debug("BsqSwap Trade at PREPARATION:\n{}", toTradeDetailTable.apply(swapTrade)); + assertEquals(PREPARATION.name(), swapTrade.getState()); + genBtcBlocksThenWait(1, 3_000); + + swapTrade = getBsqSwapTrade(bobClient, tradeId); + log.debug("BsqSwap Trade at COMPLETION:\n{}", toTradeDetailTable.apply(swapTrade)); + assertEquals(COMPLETED.name(), swapTrade.getState()); + + runCliGetClosedTrades(); + } + + @Test + @Order(4) + public void testCompletedSwapTxConfirmations() { + sleep(2_000); // Wait for TX confirmation to happen on node. + + var alicesTrade = getBsqSwapTrade(aliceClient, tradeId); + log.debug("Alice's BsqSwap Trade at COMPLETION:\n{}", toTradeDetailTable.apply(alicesTrade)); + assertEquals(1, alicesTrade.getBsqSwapTradeInfo().getNumConfirmations()); + + var bobsTrade = getBsqSwapTrade(bobClient, tradeId); + log.debug("Bob's BsqSwap Trade at COMPLETION:\n{}", toTradeDetailTable.apply(bobsTrade)); + assertEquals(1, bobsTrade.getBsqSwapTradeInfo().getNumConfirmations()); + + genBtcBlocksThenWait(1, 2_000); + + bobsTrade = getBsqSwapTrade(bobClient, tradeId); + log.debug("Bob's BsqSwap Trade at COMPLETION:\n{}", toTradeDetailTable.apply(bobsTrade)); + assertEquals(2, bobsTrade.getBsqSwapTradeInfo().getNumConfirmations()); + + runCliGetClosedTrades(); + } + + @Test + @Order(5) + public void testGetBalancesAfterTrade() { + var alicesBalances = aliceClient.getBalances(); + log.debug("Alice's After Trade Balance:\n{}", formatBalancesTbls(alicesBalances)); + var bobsBalances = bobClient.getBalances(); + log.debug("Bob's After Trade Balance:\n{}", formatBalancesTbls(bobsBalances)); + } + + private TradeInfo getBsqSwapTrade(GrpcClient client, String tradeId) { + int numFetchAttempts = 0; + while (true) { + try { + numFetchAttempts++; + return client.getTrade(tradeId); + } catch (Exception ex) { + log.warn(ex.getMessage()); + if (numFetchAttempts > 9) { + if (checkForLoggedExceptions) { + printNodeExceptionMessages(log); + } + fail(format("Could not find new bsq swap trade after %d attempts.", numFetchAttempts)); + } else { + sleep(1_000); + } + } + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java index 26ac9d24666..55487f740bd 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -133,7 +133,7 @@ public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { @Test @Order(4) - public void testKeepFunds(final TestInfo testInfo) { + public void testCloseTrade(final TestInfo testInfo) { try { genBtcBlocksThenWait(1, 1_000); var trade = aliceClient.getTrade(tradeId); @@ -147,6 +147,9 @@ public void testKeepFunds(final TestInfo testInfo) { 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); + + runCliGetClosedTrades(); + } catch (StatusRuntimeException e) { fail(e); } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyXMROfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyXMROfferTest.java index d59bc02bc6c..11f04fd890b 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyXMROfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyXMROfferTest.java @@ -152,7 +152,7 @@ public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) { @Test @Order(4) - public void testKeepFunds(final TestInfo testInfo) { + public void testCloseTrade(final TestInfo testInfo) { try { genBtcBlocksThenWait(1, 1_000); var trade = bobClient.getTrade(tradeId); diff --git a/apitest/src/test/java/bisq/apitest/scenario/LongRunningBsqSwapTest.java b/apitest/src/test/java/bisq/apitest/scenario/LongRunningBsqSwapTest.java index d56c98e5b31..42a63e0136c 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/LongRunningBsqSwapTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/LongRunningBsqSwapTest.java @@ -31,7 +31,7 @@ import bisq.apitest.method.offer.AbstractOfferTest; -import bisq.apitest.method.trade.BsqSwapTradeTest; +import bisq.apitest.method.trade.BsqSwapBuyBtcTradeTest; @EnabledIf("envLongRunningTestEnabled") @Slf4j @@ -49,7 +49,7 @@ public static void setUp() { @Order(1) public void testBsqSwaps() { // TODO Fix wallet inconsistency bugs after N(?) trades. - BsqSwapTradeTest test = new BsqSwapTradeTest(); + BsqSwapBuyBtcTradeTest test = new BsqSwapBuyBtcTradeTest(); test.setCheckForLoggedExceptions(true); for (int swapCount = 1; swapCount <= MAX_SWAPS; swapCount++) { @@ -57,7 +57,7 @@ public void testBsqSwaps() { test.testGetBalancesBeforeTrade(); - test.testAliceCreateBsqSwapBuyOffer(); + test.testAliceCreateBsqSwapBuyBtcOffer(); genBtcBlocksThenWait(1, 8_000); test.testBobTakesBsqSwapOffer(); diff --git a/apitest/src/test/java/bisq/apitest/scenario/LongRunningTradesTest.java b/apitest/src/test/java/bisq/apitest/scenario/LongRunningTradesTest.java index ae9c3ae3de0..4322a11bbd3 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/LongRunningTradesTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/LongRunningTradesTest.java @@ -71,7 +71,7 @@ public void testTakeBuyBTCOffer(final TestInfo testInfo) { test.testTakeAlicesBuyOffer(testInfo); test.testAlicesConfirmPaymentStarted(testInfo); test.testBobsConfirmPaymentReceived(testInfo); - test.testKeepFunds(testInfo); + test.testCloseTrade(testInfo); } public void testTakeSellBTCOffer(final TestInfo testInfo) { diff --git a/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java index 6e91705b4d8..cfe7e74e4aa 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java @@ -29,7 +29,8 @@ import bisq.apitest.method.trade.AbstractTradeTest; -import bisq.apitest.method.trade.BsqSwapTradeTest; +import bisq.apitest.method.trade.BsqSwapBuyBtcTradeTest; +import bisq.apitest.method.trade.BsqSwapSellBtcTradeTest; import bisq.apitest.method.trade.FailUnfailTradeTest; import bisq.apitest.method.trade.TakeBuyBSQOfferTest; import bisq.apitest.method.trade.TakeBuyBTCOfferTest; @@ -56,7 +57,7 @@ public void testTakeBuyBTCOffer(final TestInfo testInfo) { test.testTakeAlicesBuyOffer(testInfo); test.testAlicesConfirmPaymentStarted(testInfo); test.testBobsConfirmPaymentReceived(testInfo); - test.testKeepFunds(testInfo); + test.testCloseTrade(testInfo); } @Test @@ -108,7 +109,7 @@ public void testTakeBuyXMROffer(final TestInfo testInfo) { test.testTakeAlicesSellBTCForXMROffer(testInfo); test.testBobsConfirmPaymentStarted(testInfo); test.testAlicesConfirmPaymentReceived(testInfo); - test.testKeepFunds(testInfo); + test.testCloseTrade(testInfo); } @Test @@ -124,16 +125,26 @@ public void testTakeSellXMROffer(final TestInfo testInfo) { @Test @Order(8) - public void testBsqSwapTradeTest(final TestInfo testInfo) { - BsqSwapTradeTest test = new BsqSwapTradeTest(); + public void testBsqSwapBuyBtcTrade(final TestInfo testInfo) { + BsqSwapBuyBtcTradeTest test = new BsqSwapBuyBtcTradeTest(); test.testGetBalancesBeforeTrade(); - test.testAliceCreateBsqSwapBuyOffer(); + test.testAliceCreateBsqSwapBuyBtcOffer(); test.testBobTakesBsqSwapOffer(); test.testGetBalancesAfterTrade(); } @Test @Order(9) + public void testBsqSwapSellBtcTrade(final TestInfo testInfo) { + BsqSwapSellBtcTradeTest test = new BsqSwapSellBtcTradeTest(); + test.testGetBalancesBeforeTrade(); + test.testAliceCreateBsqSwapSellBtcOffer(); + test.testBobTakesBsqSwapOffer(); + test.testGetBalancesAfterTrade(); + } + + @Test + @Order(10) public void testFailUnfailTrade(final TestInfo testInfo) { FailUnfailTradeTest test = new FailUnfailTradeTest(); test.testFailAndUnFailBuyBTCTrade(testInfo); diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index e42619df6b8..4086b8cbcdf 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -47,6 +47,8 @@ import static bisq.cli.opts.OptLabel.*; import static bisq.cli.table.builder.TableType.*; import static bisq.proto.grpc.GetOfferCategoryReply.OfferCategory.BSQ_SWAP; +import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED; +import static bisq.proto.grpc.GetTradesRequest.Category.OPEN; import static java.lang.String.format; import static java.lang.System.err; import static java.lang.System.exit; @@ -67,6 +69,7 @@ import bisq.cli.opts.GetOffersOptionParser; import bisq.cli.opts.GetPaymentAcctFormOptionParser; import bisq.cli.opts.GetTradeOptionParser; +import bisq.cli.opts.GetTradesOptionParser; import bisq.cli.opts.GetTransactionOptionParser; import bisq.cli.opts.OfferIdOptionParser; import bisq.cli.opts.RegisterDisputeAgentOptionParser; @@ -503,6 +506,26 @@ public static void run(String[] args) { return; } + case gettrades: { + var opts = new GetTradesOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var category = opts.getCategory(); + var trades = category.equals(OPEN) + ? client.getOpenTrades() + : client.getTradeHistory(category); + if (trades.isEmpty()) { + out.println(format("no %s trades found", category.name().toLowerCase())); + } else { + var tableType = category.equals(OPEN) + ? OPEN_TRADES_TBL + : category.equals(CLOSED) ? CLOSED_TRADES_TBL : FAILED_TRADES_TBL; + new TableBuilder(tableType, trades).build().print(out); + } + return; + } case confirmpaymentstarted: { var opts = new GetTradeOptionParser(args).parse(); if (opts.isForHelp()) { @@ -882,6 +905,8 @@ private static void printHelp(OptionParser parser, @SuppressWarnings("SameParame stream.format(rowFormat, gettrade.name(), "--trade-id= \\", "Get trade summary or full contract"); stream.format(rowFormat, "", "[--show-contract=]", ""); stream.println(); + stream.format(rowFormat, gettrades.name(), "[--category=]", "Get open (default), closed, or failed trades"); + stream.println(); stream.format(rowFormat, confirmpaymentstarted.name(), "--trade-id=", "Confirm payment started"); stream.println(); stream.format(rowFormat, confirmpaymentreceived.name(), "--trade-id=", "Confirm payment received"); diff --git a/cli/src/main/java/bisq/cli/GrpcClient.java b/cli/src/main/java/bisq/cli/GrpcClient.java index 1d93187353d..897599c66fa 100644 --- a/cli/src/main/java/bisq/cli/GrpcClient.java +++ b/cli/src/main/java/bisq/cli/GrpcClient.java @@ -22,6 +22,7 @@ import bisq.proto.grpc.BsqBalanceInfo; import bisq.proto.grpc.BtcBalanceInfo; import bisq.proto.grpc.GetMethodHelpRequest; +import bisq.proto.grpc.GetTradesRequest; import bisq.proto.grpc.GetVersionRequest; import bisq.proto.grpc.OfferInfo; import bisq.proto.grpc.RegisterDisputeAgentRequest; @@ -364,6 +365,14 @@ public TradeInfo getTrade(String tradeId) { return tradesServiceRequest.getTrade(tradeId); } + public List getOpenTrades() { + return tradesServiceRequest.getOpenTrades(); + } + + public List getTradeHistory(GetTradesRequest.Category category) { + return tradesServiceRequest.getTradeHistory(category); + } + public void confirmPaymentStarted(String tradeId) { tradesServiceRequest.confirmPaymentStarted(tradeId); } diff --git a/cli/src/main/java/bisq/cli/Method.java b/cli/src/main/java/bisq/cli/Method.java index 55207203846..56677d7c523 100644 --- a/cli/src/main/java/bisq/cli/Method.java +++ b/cli/src/main/java/bisq/cli/Method.java @@ -42,6 +42,7 @@ public enum Method { getpaymentaccts, getpaymentmethods, gettrade, + gettrades, failtrade, unfailtrade, gettransaction, diff --git a/cli/src/main/java/bisq/cli/opts/GetTradesOptionParser.java b/cli/src/main/java/bisq/cli/opts/GetTradesOptionParser.java new file mode 100644 index 00000000000..c2f402b968c --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/GetTradesOptionParser.java @@ -0,0 +1,84 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli.opts; + + +import bisq.proto.grpc.GetTradesRequest; + +import joptsimple.OptionSpec; + +import java.util.function.Predicate; + +import static bisq.cli.opts.OptLabel.OPT_CATEGORY; +import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED; +import static bisq.proto.grpc.GetTradesRequest.Category.FAILED; +import static bisq.proto.grpc.GetTradesRequest.Category.OPEN; +import static java.util.Arrays.stream; + +public class GetTradesOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + // Map valid CLI option values to gRPC request parameters. + private enum CATEGORY { + // Lower case enum fits CLI method and parameter style. + open(OPEN), + closed(CLOSED), + failed(FAILED); + + private final GetTradesRequest.Category grpcRequestCategory; + + CATEGORY(GetTradesRequest.Category grpcRequestCategory) { + this.grpcRequestCategory = grpcRequestCategory; + } + } + + final OptionSpec categoryOpt = parser.accepts(OPT_CATEGORY, + "category of trades (open|closed|failed)") + .withRequiredArg() + .defaultsTo(CATEGORY.open.name()); + + private final Predicate isValidCategory = (c) -> + stream(CATEGORY.values()).anyMatch(v -> v.name().equalsIgnoreCase(c)); + + public GetTradesOptionParser(String[] args) { + super(args); + } + + public GetTradesOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (options.has(categoryOpt)) { + String category = options.valueOf(categoryOpt); + if (category.isEmpty()) + throw new IllegalArgumentException("no category (open|closed|failed) specified"); + + if (!isValidCategory.test(category)) + throw new IllegalArgumentException("category must be open|closed|failed"); + } + + return this; + } + + public GetTradesRequest.Category getCategory() { + String cliOpt = options.valueOf(categoryOpt); + return CATEGORY.valueOf(cliOpt).grpcRequestCategory; + } +} diff --git a/cli/src/main/java/bisq/cli/opts/OptLabel.java b/cli/src/main/java/bisq/cli/opts/OptLabel.java index 5caeb1f05e6..32b9484e845 100644 --- a/cli/src/main/java/bisq/cli/opts/OptLabel.java +++ b/cli/src/main/java/bisq/cli/opts/OptLabel.java @@ -24,6 +24,7 @@ public class OptLabel { public final static String OPT_ACCOUNT_NAME = "account-name"; public final static String OPT_ADDRESS = "address"; public final static String OPT_AMOUNT = "amount"; + public final static String OPT_CATEGORY = "category"; public final static String OPT_CURRENCY_CODE = "currency-code"; public final static String OPT_DIRECTION = "direction"; public final static String OPT_DISPUTE_AGENT_TYPE = "dispute-agent-type"; diff --git a/cli/src/main/java/bisq/cli/request/TradesServiceRequest.java b/cli/src/main/java/bisq/cli/request/TradesServiceRequest.java index 69b535a77e0..42ec3486fdd 100644 --- a/cli/src/main/java/bisq/cli/request/TradesServiceRequest.java +++ b/cli/src/main/java/bisq/cli/request/TradesServiceRequest.java @@ -22,12 +22,18 @@ import bisq.proto.grpc.ConfirmPaymentStartedRequest; import bisq.proto.grpc.FailTradeRequest; import bisq.proto.grpc.GetTradeRequest; +import bisq.proto.grpc.GetTradesRequest; import bisq.proto.grpc.TakeOfferReply; import bisq.proto.grpc.TakeOfferRequest; import bisq.proto.grpc.TradeInfo; import bisq.proto.grpc.UnFailTradeRequest; import bisq.proto.grpc.WithdrawFundsRequest; +import java.util.List; + +import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED; +import static bisq.proto.grpc.GetTradesRequest.Category.FAILED; + import bisq.cli.GrpcStubs; @@ -72,6 +78,22 @@ public TradeInfo getTrade(String tradeId) { return grpcStubs.tradesService.getTrade(request).getTrade(); } + public List getOpenTrades() { + var request = GetTradesRequest.newBuilder() + .build(); + return grpcStubs.tradesService.getTrades(request).getTradesList(); + } + + public List getTradeHistory(GetTradesRequest.Category category) { + if (!category.equals(CLOSED) && !category.equals(FAILED)) + throw new IllegalStateException("unrecognized gettrades category parameter " + category.name()); + + var request = GetTradesRequest.newBuilder() + .setCategory(category) + .build(); + return grpcStubs.tradesService.getTrades(request).getTradesList(); + } + public void confirmPaymentStarted(String tradeId) { var request = ConfirmPaymentStartedRequest.newBuilder() .setTradeId(tradeId) diff --git a/cli/src/main/java/bisq/cli/table/builder/AbstractTradeListBuilder.java b/cli/src/main/java/bisq/cli/table/builder/AbstractTradeListBuilder.java index 5c7b109e574..7c70d1d0653 100644 --- a/cli/src/main/java/bisq/cli/table/builder/AbstractTradeListBuilder.java +++ b/cli/src/main/java/bisq/cli/table/builder/AbstractTradeListBuilder.java @@ -26,7 +26,6 @@ import java.math.RoundingMode; import java.util.List; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -37,6 +36,7 @@ import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_BUYER_DEPOSIT; import static bisq.cli.table.builder.TableBuilderConstants.COL_HEADER_SELLER_DEPOSIT; import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL; +import static protobuf.OfferDirection.SELL; @@ -79,7 +79,7 @@ abstract class AbstractTradeListBuilder extends AbstractTableBuilder { @Nullable protected final Column colOfferType; @Nullable - protected final Column colStatusDescription; + protected final Column colClosingStatus; // Trade detail tbl specific columns @@ -133,7 +133,7 @@ abstract class AbstractTradeListBuilder extends AbstractTableBuilder { this.colPaymentMethod = colSupplier.paymentMethodColumn.get(); this.colRole = colSupplier.roleColumn.get(); this.colOfferType = colSupplier.offerTypeColumn.get(); - this.colStatusDescription = colSupplier.statusDescriptionColumn.get(); + this.colClosingStatus = colSupplier.statusDescriptionColumn.get(); // Trade detail specific columns, some in common with BSQ swap trades detail. @@ -168,7 +168,15 @@ protected void validate() { private final Supplier isTradeDetailTblBuilder = () -> tableType.equals(TRADE_DETAIL_TBL); protected final Predicate isFiatTrade = (t) -> isFiatOffer.test(t.getOffer()); protected final Predicate isBsqSwapTrade = (t) -> t.getOffer().getIsBsqSwapOffer(); + protected final Predicate isMyOffer = (t) -> t.getOffer().getIsMyOffer(); protected final Predicate isTaker = (t) -> t.getRole().toLowerCase().contains("taker"); + protected final Predicate isSellOffer = (t) -> t.getOffer().getDirection().equals(SELL.name()); + protected final Predicate isBtcSeller = (t) -> (isMyOffer.test(t) && isSellOffer.test(t)) + || (!isMyOffer.test(t) && !isSellOffer.test(t)); + protected final Predicate isTradeFeeBtc = (t) -> isMyOffer.test(t) + ? t.getOffer().getIsCurrencyForMakerFeeBtc() + : t.getIsCurrencyForTakerFeeBtc(); + // Column Value Functions @@ -206,12 +214,19 @@ protected void validate() { ? formatToPercent(t.getOffer().getMarketPriceMargin()) : "N/A"; - protected final Function toMyMinerTxFee = (t) -> - isTaker.test(t) + protected final Function toMyMinerTxFee = (t) -> { + if (isBsqSwapTrade.test(t)) { + // The BTC seller pays the miner fee for both sides. + return isBtcSeller.test(t) ? t.getTxFeeAsLong() : 0L; + } else { + return isTaker.test(t) ? t.getTxFeeAsLong() : t.getOffer().getTxFee(); + } + }; - protected final BiFunction toTradeFeeBsq = (t, isMyOffer) -> { + protected final Function toTradeFeeBsq = (t) -> { + var isMyOffer = t.getOffer().getIsMyOffer(); if (isMyOffer) { return t.getOffer().getIsCurrencyForMakerFeeBtc() ? 0L // Maker paid BTC fee, return 0. @@ -223,7 +238,8 @@ protected void validate() { } }; - protected final BiFunction toTradeFeeBtc = (t, isMyOffer) -> { + protected final Function toTradeFeeBtc = (t) -> { + var isMyOffer = t.getOffer().getIsMyOffer(); if (isMyOffer) { return t.getOffer().getIsCurrencyForMakerFeeBtc() ? t.getOffer().getMakerFee() diff --git a/cli/src/main/java/bisq/cli/table/builder/AbstractTradeTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/AbstractTradeTableBuilder.java deleted file mode 100644 index f94c19f63ec..00000000000 --- a/cli/src/main/java/bisq/cli/table/builder/AbstractTradeTableBuilder.java +++ /dev/null @@ -1,106 +0,0 @@ -package bisq.cli.table.builder; - -import bisq.proto.grpc.TradeInfo; - -import java.util.List; -import java.util.function.Function; -import java.util.function.Predicate; - -import javax.annotation.Nullable; - -import static bisq.cli.table.builder.TableBuilderConstants.*; -import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL; -import static bisq.cli.table.column.Column.JUSTIFICATION.RIGHT; - - - -import bisq.cli.table.column.BtcColumn; -import bisq.cli.table.column.Column; -import bisq.cli.table.column.Iso8601DateTimeColumn; -import bisq.cli.table.column.MixedPriceColumn; -import bisq.cli.table.column.MixedTradeFeeColumn; -import bisq.cli.table.column.MixedVolumeColumn; -import bisq.cli.table.column.SatoshiColumn; -import bisq.cli.table.column.StringColumn; - -/** - * Builds a {@code bisq.cli.table.Table} from one or more {@code bisq.proto.grpc.TradeInfo} objects. - */ -abstract class AbstractTradeTableBuilder extends AbstractTableBuilder { - - @Nullable - protected final Column colTradeId; - @Nullable - protected final Column colCreateDate; - @Nullable - protected final Column colMarket; - @Nullable - protected final MixedPriceColumn colMixedPrice; - @Nullable - protected final Column colPriceDeviation; - @Nullable - protected final Column colAmountInBtc; - @Nullable - protected final MixedVolumeColumn colMixedAmount; - @Nullable - protected final Column colCurrency; - @Nullable - protected final MixedTradeFeeColumn colMixedTradeFee; - @Nullable - protected final Column colBuyerDeposit; - @Nullable - protected final Column colSellerDeposit; - @Nullable - protected final Column colOfferType; - @Nullable - protected final Column colStatus; - protected final Column colMinerTxFee; - - AbstractTradeTableBuilder(TableType tableType, List protos) { - super(tableType, protos); - boolean isTradeDetail = tableType.equals(TRADE_DETAIL_TBL); - this.colTradeId = isTradeDetail ? null : new StringColumn(COL_HEADER_TRADE_ID); - this.colCreateDate = isTradeDetail ? null : new Iso8601DateTimeColumn(COL_HEADER_DATE_TIME); - this.colMarket = isTradeDetail ? null : new StringColumn(COL_HEADER_MARKET); - this.colMixedPrice = isTradeDetail ? null : new MixedPriceColumn(COL_HEADER_PRICE); - this.colPriceDeviation = isTradeDetail ? null : new StringColumn(COL_HEADER_DEVIATION, RIGHT); - this.colAmountInBtc = isTradeDetail ? null : new BtcColumn(COL_HEADER_AMOUNT_IN_BTC); - this.colMixedAmount = isTradeDetail ? null : new MixedVolumeColumn(COL_HEADER_AMOUNT); - this.colCurrency = isTradeDetail ? null : new StringColumn(COL_HEADER_CURRENCY); - this.colMixedTradeFee = isTradeDetail ? null : new MixedTradeFeeColumn(COL_HEADER_TRADE_FEE); - this.colBuyerDeposit = isTradeDetail ? null : new SatoshiColumn(COL_HEADER_BUYER_DEPOSIT); - this.colSellerDeposit = isTradeDetail ? null : new SatoshiColumn(COL_HEADER_SELLER_DEPOSIT); - this.colOfferType = isTradeDetail ? null : new StringColumn(COL_HEADER_OFFER_TYPE); - this.colStatus = isTradeDetail ? null : new StringColumn(COL_HEADER_STATUS); - this.colMinerTxFee = new SatoshiColumn(COL_HEADER_TX_FEE); - } - - protected final Predicate isFiatTrade = (t) -> isFiatOffer.test(t.getOffer()); - - protected final Predicate isTaker = (t) -> t.getRole().toLowerCase().contains("taker"); - - protected final Function toPaymentCurrencyCode = (t) -> - isFiatTrade.test(t) - ? t.getOffer().getCounterCurrencyCode() - : t.getOffer().getBaseCurrencyCode(); - - protected final Function toAmount = (t) -> - isFiatTrade.test(t) - ? t.getTradeAmountAsLong() - : t.getTradeVolume(); - - protected final Function toMinerTxFee = (t) -> - isTaker.test(t) - ? t.getTxFeeAsLong() - : t.getOffer().getTxFee(); - - protected final Function toMakerTakerFee = (t) -> - isTaker.test(t) - ? t.getTakerFeeAsLong() - : t.getOffer().getMakerFee(); - - protected final Function toTradeCost = (t) -> - isFiatTrade.test(t) - ? t.getTradeVolume() - : t.getTradeAmountAsLong(); -} diff --git a/cli/src/main/java/bisq/cli/table/builder/ClosedTradeTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/ClosedTradeTableBuilder.java new file mode 100644 index 00000000000..261cddc1117 --- /dev/null +++ b/cli/src/main/java/bisq/cli/table/builder/ClosedTradeTableBuilder.java @@ -0,0 +1,82 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli.table.builder; + +import java.util.List; + +import static bisq.cli.table.builder.TableType.CLOSED_TRADES_TBL; + + + +import bisq.cli.table.Table; +import bisq.cli.table.column.MixedPriceColumn; + +class ClosedTradeTableBuilder extends AbstractTradeListBuilder { + + ClosedTradeTableBuilder(List protos) { + super(CLOSED_TRADES_TBL, protos); + } + + public Table build() { + populateColumns(); + return new Table(colTradeId, + colCreateDate.asStringColumn(), + colMarket, + colPrice.asStringColumn(), + colPriceDeviation.justify(), + colAmountInBtc.asStringColumn(), + colMixedAmount.asStringColumn(), + colCurrency, + colMinerTxFee.asStringColumn(), + colMixedTradeFee.asStringColumn(), + colBuyerDeposit.asStringColumn(), + colSellerDeposit.asStringColumn(), + colOfferType, + colClosingStatus); + } + + private void populateColumns() { + trades.stream().forEachOrdered(t -> { + colTradeId.addRow(t.getTradeId()); + colCreateDate.addRow(t.getDate()); + colMarket.addRow(toMarket.apply(t)); + ((MixedPriceColumn) colPrice).addRow(t.getTradePrice(), isFiatTrade.test(t)); + colPriceDeviation.addRow(toPriceDeviation.apply(t)); + colAmountInBtc.addRow(t.getTradeAmountAsLong()); + colMixedAmount.addRow(t.getTradeVolume(), toDisplayedVolumePrecision.apply(t)); + colCurrency.addRow(toPaymentCurrencyCode.apply(t)); + colMinerTxFee.addRow(toMyMinerTxFee.apply(t)); + + if (t.getOffer().getIsBsqSwapOffer()) { + // For BSQ Swaps, BTC buyer pays the BSQ trade fee for both sides (BTC seller pays no fee). + var optionalTradeFeeBsq = isBtcSeller.test(t) ? 0L : toTradeFeeBsq.apply(t); + colMixedTradeFee.addRow(optionalTradeFeeBsq, true); + } else if (isTradeFeeBtc.test(t)) { + colMixedTradeFee.addRow(toTradeFeeBtc.apply(t), false); + } else { + // V1 trade fee paid in BSQ. + colMixedTradeFee.addRow(toTradeFeeBsq.apply(t), true); + } + + colBuyerDeposit.addRow(t.getOffer().getBuyerSecurityDeposit()); + colSellerDeposit.addRow(t.getOffer().getSellerSecurityDeposit()); + colOfferType.addRow(toOfferType.apply(t)); + colClosingStatus.addRow(t.getClosingStatus()); + }); + } +} diff --git a/cli/src/main/java/bisq/cli/table/builder/FailedTradeTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/FailedTradeTableBuilder.java index a4a60843476..13b4399070a 100644 --- a/cli/src/main/java/bisq/cli/table/builder/FailedTradeTableBuilder.java +++ b/cli/src/main/java/bisq/cli/table/builder/FailedTradeTableBuilder.java @@ -1,8 +1,25 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + package bisq.cli.table.builder; import java.util.List; -import static bisq.cli.table.builder.TableType.FAILED_TRADE_TBL; +import static bisq.cli.table.builder.TableType.FAILED_TRADES_TBL; @@ -15,7 +32,7 @@ class FailedTradeTableBuilder extends AbstractTradeListBuilder { FailedTradeTableBuilder(List protos) { - super(FAILED_TRADE_TBL, protos); + super(FAILED_TRADES_TBL, protos); } public Table build() { @@ -29,7 +46,7 @@ public Table build() { colCurrency, colOfferType, colRole, - colStatusDescription); + colClosingStatus); } private void populateColumns() { @@ -43,7 +60,7 @@ private void populateColumns() { colCurrency.addRow(toPaymentCurrencyCode.apply(t)); colOfferType.addRow(toOfferType.apply(t)); colRole.addRow(t.getRole()); - colStatusDescription.addRow("Failed"); + colClosingStatus.addRow("Failed"); }); } } diff --git a/cli/src/main/java/bisq/cli/table/builder/OpenTradeTableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/OpenTradeTableBuilder.java index a388d036640..b9c3f7df5da 100644 --- a/cli/src/main/java/bisq/cli/table/builder/OpenTradeTableBuilder.java +++ b/cli/src/main/java/bisq/cli/table/builder/OpenTradeTableBuilder.java @@ -1,8 +1,25 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + package bisq.cli.table.builder; import java.util.List; -import static bisq.cli.table.builder.TableType.OPEN_TRADE_TBL; +import static bisq.cli.table.builder.TableType.OPEN_TRADES_TBL; @@ -15,7 +32,7 @@ class OpenTradeTableBuilder extends AbstractTradeListBuilder { OpenTradeTableBuilder(List protos) { - super(OPEN_TRADE_TBL, protos); + super(OPEN_TRADES_TBL, protos); } public Table build() { diff --git a/cli/src/main/java/bisq/cli/table/builder/TableBuilder.java b/cli/src/main/java/bisq/cli/table/builder/TableBuilder.java index 4084be33a95..566d3b2a436 100644 --- a/cli/src/main/java/bisq/cli/table/builder/TableBuilder.java +++ b/cli/src/main/java/bisq/cli/table/builder/TableBuilder.java @@ -49,14 +49,14 @@ public Table build() { return new BsqBalanceTableBuilder(protos).build(); case BTC_BALANCE_TBL: return new BtcBalanceTableBuilder(protos).build(); - case CLOSED_TRADE_TBL: - throw new UnsupportedOperationException("TODO return new ClosedTradeTableBuilder(protos).build()"); - case FAILED_TRADE_TBL: - throw new UnsupportedOperationException("TODO return new FailedTradeTableBuilder(protos).build()"); + case CLOSED_TRADES_TBL: + return new ClosedTradeTableBuilder(protos).build(); + case FAILED_TRADES_TBL: + return new FailedTradeTableBuilder(protos).build(); case OFFER_TBL: return new OfferTableBuilder(protos).build(); - case OPEN_TRADE_TBL: - throw new UnsupportedOperationException("TODO return new OpenTradeTableBuilder(protos).build()"); + case OPEN_TRADES_TBL: + return new OpenTradeTableBuilder(protos).build(); case PAYMENT_ACCOUNT_TBL: return new PaymentAccountTableBuilder(protos).build(); case TRADE_DETAIL_TBL: diff --git a/cli/src/main/java/bisq/cli/table/builder/TableBuilderConstants.java b/cli/src/main/java/bisq/cli/table/builder/TableBuilderConstants.java index b29aa3e3ec9..33800b5d945 100644 --- a/cli/src/main/java/bisq/cli/table/builder/TableBuilderConstants.java +++ b/cli/src/main/java/bisq/cli/table/builder/TableBuilderConstants.java @@ -36,8 +36,8 @@ class TableBuilderConstants { static final String COL_HEADER_UNLOCKING_BONDS_BALANCE = "Unlocking Bonds Balance"; static final String COL_HEADER_UNVERIFIED_BALANCE = "Unverified Balance"; static final String COL_HEADER_BSQ_SWAP_TRADE_ROLE = "My BSQ Swap Role"; - static final String COL_HEADER_BUYER_DEPOSIT = "Buyer Deposit"; - static final String COL_HEADER_SELLER_DEPOSIT = "Seller Deposit"; + static final String COL_HEADER_BUYER_DEPOSIT = "Buyer Deposit (BTC)"; + static final String COL_HEADER_SELLER_DEPOSIT = "Seller Deposit (BTC)"; static final String COL_HEADER_CONFIRMATIONS = "Confirmations"; static final String COL_HEADER_DEVIATION = "Deviation"; static final String COL_HEADER_IS_USED_ADDRESS = "Is Used"; diff --git a/cli/src/main/java/bisq/cli/table/builder/TableType.java b/cli/src/main/java/bisq/cli/table/builder/TableType.java index 0f6afef25b9..a08003335c2 100644 --- a/cli/src/main/java/bisq/cli/table/builder/TableType.java +++ b/cli/src/main/java/bisq/cli/table/builder/TableType.java @@ -25,10 +25,10 @@ public enum TableType { ADDRESS_BALANCE_TBL, BSQ_BALANCE_TBL, BTC_BALANCE_TBL, - CLOSED_TRADE_TBL, - FAILED_TRADE_TBL, + CLOSED_TRADES_TBL, + FAILED_TRADES_TBL, OFFER_TBL, - OPEN_TRADE_TBL, + OPEN_TRADES_TBL, PAYMENT_ACCOUNT_TBL, TRADE_DETAIL_TBL, TRANSACTION_TBL diff --git a/cli/src/main/java/bisq/cli/table/builder/TradeTableColumnSupplier.java b/cli/src/main/java/bisq/cli/table/builder/TradeTableColumnSupplier.java index ae03cb14e87..9f3155fa2c1 100644 --- a/cli/src/main/java/bisq/cli/table/builder/TradeTableColumnSupplier.java +++ b/cli/src/main/java/bisq/cli/table/builder/TradeTableColumnSupplier.java @@ -32,9 +32,9 @@ import javax.annotation.Nullable; import static bisq.cli.table.builder.TableBuilderConstants.*; -import static bisq.cli.table.builder.TableType.CLOSED_TRADE_TBL; -import static bisq.cli.table.builder.TableType.FAILED_TRADE_TBL; -import static bisq.cli.table.builder.TableType.OPEN_TRADE_TBL; +import static bisq.cli.table.builder.TableType.CLOSED_TRADES_TBL; +import static bisq.cli.table.builder.TableType.FAILED_TRADES_TBL; +import static bisq.cli.table.builder.TableType.OPEN_TRADES_TBL; import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL; import static bisq.cli.table.column.AltcoinColumn.DISPLAY_MODE.ALTCOIN_OFFER_VOLUME; import static bisq.cli.table.column.Column.JUSTIFICATION.LEFT; @@ -75,9 +75,9 @@ public TradeTableColumnSupplier(TableType tableType, List trades) { } private final Supplier isTradeDetailTblBuilder = () -> getTableType().equals(TRADE_DETAIL_TBL); - private final Supplier isOpenTradeTblBuilder = () -> getTableType().equals(OPEN_TRADE_TBL); - private final Supplier isClosedTradeTblBuilder = () -> getTableType().equals(CLOSED_TRADE_TBL); - private final Supplier isFailedTradeTblBuilder = () -> getTableType().equals(FAILED_TRADE_TBL); + private final Supplier isOpenTradeTblBuilder = () -> getTableType().equals(OPEN_TRADES_TBL); + private final Supplier isClosedTradeTblBuilder = () -> getTableType().equals(CLOSED_TRADES_TBL); + private final Supplier isFailedTradeTblBuilder = () -> getTableType().equals(FAILED_TRADES_TBL); private final Supplier firstRow = () -> getTrades().get(0); private final Predicate isFiatOffer = (o) -> o.getBaseCurrencyCode().equals("BTC"); private final Predicate isFiatTrade = (t) -> isFiatOffer.test(t.getOffer()); diff --git a/cli/src/test/java/bisq/cli/GetTradesSmokeTest.java b/cli/src/test/java/bisq/cli/GetTradesSmokeTest.java new file mode 100644 index 00000000000..a95b734a870 --- /dev/null +++ b/cli/src/test/java/bisq/cli/GetTradesSmokeTest.java @@ -0,0 +1,52 @@ +package bisq.cli; + +import bisq.proto.grpc.TradeInfo; + +import java.util.List; + +import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL; +import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED; +import static java.lang.System.out; + + + +import bisq.cli.table.builder.TableBuilder; + +@SuppressWarnings("unused") +public class GetTradesSmokeTest extends AbstractCliTest { + + public static void main(String[] args) { + GetTradesSmokeTest test = new GetTradesSmokeTest(); + test.printAlicesTrades(); + test.printBobsTrades(); + } + + private final List openTrades; + private final List closedTrades; + + public GetTradesSmokeTest() { + super(); + this.openTrades = aliceClient.getOpenTrades(); + this.closedTrades = aliceClient.getTradeHistory(CLOSED); + } + + private void printAlicesTrades() { + out.println("ALICE'S OPEN TRADES"); + openTrades.stream().forEachOrdered(t -> printTrade(aliceClient, t.getTradeId())); + out.println("ALICE'S CLOSED TRADES"); + closedTrades.stream().forEachOrdered(t -> printTrade(aliceClient, t.getTradeId())); + } + + private void printBobsTrades() { + out.println("BOB'S OPEN TRADES"); + openTrades.stream().forEachOrdered(t -> printTrade(bobClient, t.getTradeId())); + out.println("BOB'S CLOSED TRADES"); + closedTrades.stream().forEachOrdered(t -> printTrade(bobClient, t.getTradeId())); + } + + private void printTrade(GrpcClient client, String tradeId) { + var trade = client.getTrade(tradeId); + var tbl = new TableBuilder(TRADE_DETAIL_TBL, trade).build().toString(); + out.println(tbl); + } +} diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 57c9a07a503..5740d47b88f 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -26,6 +26,7 @@ import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentMethod; import bisq.core.trade.bisq_v1.TradeResultHandler; +import bisq.core.trade.model.Tradable; import bisq.core.trade.model.TradeModel; import bisq.core.trade.model.bisq_v1.Trade; import bisq.core.trade.model.bsq_swap.BsqSwapTrade; @@ -37,6 +38,8 @@ import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; +import bisq.proto.grpc.GetTradesRequest; + import org.bitcoinj.core.Transaction; import javax.inject.Inject; @@ -327,12 +330,16 @@ public TradeModel getTradeModel(String tradeId) { return coreTradesService.getTradeModel(tradeId); } - public String getTradeRole(String tradeId) { - return coreTradesService.getTradeRole(tradeId); + public List getOpenTrades() { + return coreTradesService.getOpenTrades(); + } + + public List getTradeHistory(GetTradesRequest.Category category) { + return coreTradesService.getTradeHistory(category); } - public String getBsqSwapTradeRole(BsqSwapTrade bsqSwapTrade) { - return coreTradesService.getBsqSwapTradeRole(bsqSwapTrade); + public String getTradeRole(TradeModel tradeModel) { + return coreTradesService.getTradeRole(tradeModel); } public void failTrade(String tradeId) { @@ -343,6 +350,14 @@ public void unFailTrade(String tradeId) { coreTradesService.unFailTrade(tradeId); } + public List getCanceledOpenOffers() { + return coreTradesService.getCanceledOpenOffers(); + } + + public String getClosedTradeStateAsString(Tradable tradable) { + return coreTradesService.getClosedTradeStateAsString(tradable); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Wallets /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index d28ce99c736..db206e2b4b4 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -21,13 +21,16 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.Offer; import bisq.core.offer.OfferUtil; +import bisq.core.offer.OpenOffer; import bisq.core.offer.bisq_v1.TakeOfferModel; import bisq.core.offer.bsq_swap.BsqSwapTakeOfferModel; +import bisq.core.trade.ClosedTradableFormatter; import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.TradeManager; import bisq.core.trade.bisq_v1.FailedTradesManager; import bisq.core.trade.bisq_v1.TradeResultHandler; import bisq.core.trade.bisq_v1.TradeUtil; +import bisq.core.trade.bsq_swap.BsqSwapTradeManager; import bisq.core.trade.model.Tradable; import bisq.core.trade.model.TradeModel; import bisq.core.trade.model.bisq_v1.Trade; @@ -39,17 +42,22 @@ import bisq.common.handlers.ErrorMessageHandler; +import bisq.proto.grpc.GetTradesRequest; + import org.bitcoinj.core.Coin; import javax.inject.Inject; import javax.inject.Singleton; +import java.util.List; import java.util.Optional; import java.util.function.Consumer; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import static bisq.core.btc.model.AddressEntry.Context.TRADE_PAYOUT; +import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED; import static java.lang.String.format; @Singleton @@ -63,7 +71,9 @@ class CoreTradesService { private final CoreWalletsService coreWalletsService; private final BtcWalletService btcWalletService; private final OfferUtil offerUtil; + private final BsqSwapTradeManager bsqSwapTradeManager; private final ClosedTradableManager closedTradableManager; + private final ClosedTradableFormatter closedTradableFormatter; private final FailedTradesManager failedTradesManager; private final TakeOfferModel takeOfferModel; private final BsqSwapTakeOfferModel bsqSwapTakeOfferModel; @@ -76,7 +86,9 @@ public CoreTradesService(CoreContext coreContext, CoreWalletsService coreWalletsService, BtcWalletService btcWalletService, OfferUtil offerUtil, + BsqSwapTradeManager bsqSwapTradeManager, ClosedTradableManager closedTradableManager, + ClosedTradableFormatter closedTradableFormatter, FailedTradesManager failedTradesManager, TakeOfferModel takeOfferModel, BsqSwapTakeOfferModel bsqSwapTakeOfferModel, @@ -87,7 +99,9 @@ public CoreTradesService(CoreContext coreContext, this.coreWalletsService = coreWalletsService; this.btcWalletService = btcWalletService; this.offerUtil = offerUtil; + this.bsqSwapTradeManager = bsqSwapTradeManager; this.closedTradableManager = closedTradableManager; + this.closedTradableFormatter = closedTradableFormatter; this.failedTradesManager = failedTradesManager; this.takeOfferModel = takeOfferModel; this.bsqSwapTakeOfferModel = bsqSwapTakeOfferModel; @@ -252,16 +266,18 @@ TradeModel getTradeModel(String tradeId) { new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); } - String getBsqSwapTradeRole(BsqSwapTrade bsqSwapTrade) { - coreWalletsService.verifyWalletsAreAvailable(); - coreWalletsService.verifyEncryptedWalletIsUnlocked(); - return tradeUtil.getRole(bsqSwapTrade); - } - - String getTradeRole(String tradeId) { + String getTradeRole(TradeModel tradeModel) { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); - return tradeUtil.getRole(getTrade(tradeId)); + var isBsqSwapTrade = tradeModel instanceof BsqSwapTrade; + try { + return isBsqSwapTrade + ? tradeUtil.getRole((BsqSwapTrade) tradeModel) + : tradeUtil.getRole((Trade) tradeModel); + } catch (Exception ex) { + log.error("Role not found for trade with Id {}.", tradeModel.getId(), ex); + return "Not Available"; + } } Trade getTrade(String tradeId) { @@ -273,15 +289,32 @@ Trade getTrade(String tradeId) { )); } + List getOpenTrades() { + coreWalletsService.verifyWalletsAreAvailable(); + coreWalletsService.verifyEncryptedWalletIsUnlocked(); + return tradeManager.getTrades().stream().collect(Collectors.toList()); + } + + List getTradeHistory(GetTradesRequest.Category category) { + coreWalletsService.verifyWalletsAreAvailable(); + coreWalletsService.verifyEncryptedWalletIsUnlocked(); + if (category.equals(CLOSED)) { + var closedTrades = closedTradableManager.getClosedTrades().stream() + .map(t -> (TradeModel) t) + .collect(Collectors.toList()); + closedTrades.addAll(bsqSwapTradeManager.getBsqSwapTrades()); + return closedTrades; + } else { + var failedV1Trades = failedTradesManager.getTrades(); + return failedV1Trades.stream().collect(Collectors.toList()); + } + } + void failTrade(String tradeId) { - // TODO Recommend that API users should use this method with extra care because + // TODO Recommend API users call this method with extra care because // the API lacks methods for diagnosing trade problems, and does not support // interaction with mediators. Users may accidentally fail valid trades, // although they can easily be un-failed with the 'unfailtrade' method. - // The 'failtrade' and 'unfailtrade' methods are implemented at this early - // stage of API development to help efficiently test a new - // 'gettrades --category=' - // method currently in development. coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); @@ -304,6 +337,14 @@ void unFailTrade(String tradeId) { }); } + List getCanceledOpenOffers() { + return closedTradableManager.getCanceledOpenOffers(); + } + + String getClosedTradeStateAsString(Tradable tradable) { + return closedTradableFormatter.getStateAsString(tradable); + } + private Optional getOpenTrade(String tradeId) { return tradeManager.getTradeById(tradeId); } diff --git a/core/src/main/java/bisq/core/api/model/BsqSwapTradeInfo.java b/core/src/main/java/bisq/core/api/model/BsqSwapTradeInfo.java index d46cf877a02..6780d9e2895 100644 --- a/core/src/main/java/bisq/core/api/model/BsqSwapTradeInfo.java +++ b/core/src/main/java/bisq/core/api/model/BsqSwapTradeInfo.java @@ -25,6 +25,9 @@ import lombok.EqualsAndHashCode; import lombok.Getter; +import static bisq.core.offer.OfferDirection.BUY; +import static bisq.core.offer.OfferDirection.SELL; + @EqualsAndHashCode @Getter public class BsqSwapTradeInfo implements Payload { @@ -41,6 +44,8 @@ public class BsqSwapTradeInfo implements Payload { private final String takerBtcAddress; private final long numConfirmations; private final String errorMessage; + private final long payout; + private final long swapPeerPayout; public BsqSwapTradeInfo(BsqSwapTradeInfoBuilder builder) { this.txId = builder.getTxId(); @@ -55,6 +60,8 @@ public BsqSwapTradeInfo(BsqSwapTradeInfoBuilder builder) { this.takerBtcAddress = builder.getTakerBtcAddress(); this.numConfirmations = builder.getNumConfirmations(); this.errorMessage = builder.getErrorMessage(); + this.payout = builder.getPayout(); + this.swapPeerPayout = builder.getSwapPeerPayout(); } public static BsqSwapTradeInfo toBsqSwapTradeInfo(BsqSwapTrade trade, @@ -66,12 +73,20 @@ public static BsqSwapTradeInfo toBsqSwapTradeInfo(BsqSwapTrade trade, var makerBtcAddress = wasMyOffer ? protocolModel.getBtcAddress() : swapPeer.getBtcAddress(); var takerBsqAddress = wasMyOffer ? swapPeer.getBsqAddress() : protocolModel.getBsqAddress(); var takerBtcAddress = wasMyOffer ? swapPeer.getBtcAddress() : protocolModel.getBtcAddress(); + // A BSQ Swap trade fee is paid in full by the BTC buyer (selling BSQ). + // The transferred BSQ (payout) is reduced by the fee of the peer. + var makerTradeFee = wasMyOffer && trade.getOffer().getDirection().equals(BUY) + ? trade.getMakerFeeAsLong() + : 0L; + var takerTradeFee = !wasMyOffer && trade.getOffer().getDirection().equals(SELL) + ? trade.getTakerFeeAsLong() + : 0L; return new BsqSwapTradeInfoBuilder() .withTxId(trade.getTxId()) .withBsqTradeAmount(trade.getBsqTradeAmount()) .withBtcTradeAmount(trade.getAmountAsLong()) - .withBsqMakerTradeFee(trade.getMakerFeeAsLong()) - .withBsqTakerTradeFee(trade.getTakerFeeAsLong()) + .withBsqMakerTradeFee(makerTradeFee) + .withBsqTakerTradeFee(takerTradeFee) .withTxFeePerVbyte(trade.getTxFeePerVbyte()) .withMakerBsqAddress(makerBsqAddress) .withMakerBtcAddress(makerBtcAddress) @@ -79,6 +94,8 @@ public static BsqSwapTradeInfo toBsqSwapTradeInfo(BsqSwapTrade trade, .withTakerBtcAddress(takerBtcAddress) .withNumConfirmations(numConfirmations) .withErrorMessage(trade.getErrorMessage()) + .withPayout(protocolModel.getPayout()) + .withSwapPeerPayout(protocolModel.getTradePeer().getPayout()) .build(); } @@ -101,6 +118,9 @@ public bisq.proto.grpc.BsqSwapTradeInfo toProtoMessage() { .setTakerBtcAddress(takerBtcAddress != null ? takerBtcAddress : "") .setTakerBtcAddress(takerBtcAddress != null ? takerBtcAddress : "") .setNumConfirmations(numConfirmations) + .setErrorMessage(errorMessage != null ? errorMessage : "") + .setPayout(payout) + .setSwapPeerPayout(swapPeerPayout) .build(); } @@ -118,6 +138,8 @@ public static BsqSwapTradeInfo fromProto(bisq.proto.grpc.BsqSwapTradeInfo proto) .withTakerBtcAddress(proto.getTakerBtcAddress()) .withNumConfirmations(proto.getNumConfirmations()) .withErrorMessage(proto.getErrorMessage()) + .withPayout(proto.getPayout()) + .withSwapPeerPayout(proto.getSwapPeerPayout()) .build(); } @@ -136,6 +158,8 @@ public String toString() { ", takerBtcAddress='" + takerBtcAddress + '\'' + ", numConfirmations='" + numConfirmations + '\'' + ", errorMessage='" + errorMessage + '\'' + + ", payout=" + payout + + ", swapPeerPayout=" + swapPeerPayout + '}'; } } diff --git a/core/src/main/java/bisq/core/api/model/CanceledTradeInfo.java b/core/src/main/java/bisq/core/api/model/CanceledTradeInfo.java new file mode 100644 index 00000000000..d44af9b0356 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/CanceledTradeInfo.java @@ -0,0 +1,72 @@ +/* + * 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.core.api.model; + +import bisq.core.api.model.builder.TradeInfoV1Builder; +import bisq.core.offer.Offer; +import bisq.core.offer.OpenOffer; + +import static bisq.core.api.model.ContractInfo.emptyContract; +import static bisq.core.api.model.OfferInfo.toMyOfferInfo; +import static bisq.core.offer.OpenOffer.State.CANCELED; +import static java.lang.String.format; +import static org.apache.commons.lang3.StringUtils.capitalize; + +/** + * Builds a TradeInfo instance from an OpenOffer with State = CANCELED. + */ +public class CanceledTradeInfo { + + public static TradeInfo toCanceledTradeInfo(OpenOffer myCanceledOpenOffer) { + if (!myCanceledOpenOffer.getState().equals(CANCELED)) + throw new IllegalArgumentException(format("offer '%s' is not canceled", myCanceledOpenOffer.getId())); + + Offer offer = myCanceledOpenOffer.getOffer(); + OfferInfo offerInfo = toMyOfferInfo(offer); + + return new TradeInfoV1Builder() // TODO May need to use BsqSwapTradeInfoBuilder? + .withOffer(offerInfo) + .withTradeId(myCanceledOpenOffer.getId()) + .withShortId(myCanceledOpenOffer.getShortId()) + .withDate(myCanceledOpenOffer.getDate().getTime()) + .withRole("") + .withIsCurrencyForTakerFeeBtc(offer.isCurrencyForMakerFeeBtc()) + .withTxFeeAsLong(offer.getTxFee().value) + .withTakerFeeAsLong(offer.getMakerFee().value) + .withTakerFeeTxId("") // Ignored + .withDepositTxId("") // Ignored + .withPayoutTxId("") // Ignored + .withTradeAmountAsLong(0) // Ignored + .withTradePrice(offer.getPrice().getValue()) + .withTradeVolume(0) // Ignored + .withTradingPeerNodeAddress("") // Ignored + .withState("") // Ignored + .withPhase("") // Ignored + .withTradePeriodState("") // Ignored + .withIsDepositPublished(false) // Ignored + .withIsDepositConfirmed(false) // Ignored + .withIsFiatSent(false) // Ignored + .withIsFiatReceived(false) // Ignored + .withIsPayoutPublished(false) // Ignored + .withIsWithdrawn(false) // Ignored + .withContractAsJson("") // Ignored + .withContract(emptyContract.get()) // Ignored + .withClosingStatus(capitalize(CANCELED.name().toLowerCase())) + .build(); + } +} diff --git a/core/src/main/java/bisq/core/api/model/TradeInfo.java b/core/src/main/java/bisq/core/api/model/TradeInfo.java index b8eb474e6ed..0bf0d791916 100644 --- a/core/src/main/java/bisq/core/api/model/TradeInfo.java +++ b/core/src/main/java/bisq/core/api/model/TradeInfo.java @@ -32,6 +32,8 @@ import static bisq.core.api.model.OfferInfo.toMyOfferInfo; import static bisq.core.api.model.OfferInfo.toOfferInfo; import static bisq.core.api.model.PaymentAccountPayloadInfo.toPaymentAccountPayloadInfo; +import static bisq.core.offer.OfferDirection.BUY; +import static bisq.core.offer.OfferDirection.SELL; import static java.util.Objects.requireNonNull; @EqualsAndHashCode @@ -71,6 +73,7 @@ public class TradeInfo implements Payload { private final ContractInfo contract; // Optional BSQ swap trade protocol details (post v1). private BsqSwapTradeInfo bsqSwapTradeInfo; + private final String closingStatus; public TradeInfo(TradeInfoV1Builder builder) { this.offer = builder.getOffer(); @@ -100,23 +103,27 @@ public TradeInfo(TradeInfoV1Builder builder) { this.contractAsJson = builder.getContractAsJson(); this.contract = builder.getContract(); this.bsqSwapTradeInfo = null; + this.closingStatus = builder.getClosingStatus(); } public static TradeInfo toNewTradeInfo(BsqSwapTrade trade, String role) { // Always called by the taker, isMyOffer=false. - return toTradeInfo(trade, role, false, 0); + return toTradeInfo(trade, role, false, 0, "Pending"); } public static TradeInfo toNewTradeInfo(Trade trade) { // Always called by the taker, isMyOffer=false. - return toTradeInfo(trade, null, false); + return toTradeInfo(trade, null, false, "Pending"); } - public static TradeInfo toTradeInfo(TradeModel tradeModel, String role, boolean isMyOffer) { + public static TradeInfo toTradeInfo(TradeModel tradeModel, + String role, + boolean isMyOffer, + String closingStatus) { if (tradeModel instanceof Trade) - return toTradeInfo((Trade) tradeModel, role, isMyOffer); + return toTradeInfo((Trade) tradeModel, role, isMyOffer, closingStatus); else if (tradeModel instanceof BsqSwapTrade) - return toTradeInfo(tradeModel, role, isMyOffer); + return toTradeInfo(tradeModel, role, isMyOffer, closingStatus); else throw new IllegalStateException("unsupported trade type: " + tradeModel.getClass().getSimpleName()); } @@ -124,8 +131,21 @@ else if (tradeModel instanceof BsqSwapTrade) public static TradeInfo toTradeInfo(BsqSwapTrade bsqSwapTrade, String role, boolean isMyOffer, - int numConfirmations) { + int numConfirmations, + String closingStatus) { OfferInfo offerInfo = isMyOffer ? toMyOfferInfo(bsqSwapTrade.getOffer()) : toOfferInfo(bsqSwapTrade.getOffer()); + // A BSQ Swap miner tx fee is paid in full by the BTC seller (buying BSQ). + // The BTC buyer's payout = tradeamount minus his share of miner fee. + var isBtcSeller = (isMyOffer && bsqSwapTrade.getOffer().getDirection().equals(SELL)) + || (!isMyOffer && bsqSwapTrade.getOffer().getDirection().equals(BUY)); + var txFeeInBtc = isBtcSeller + ? bsqSwapTrade.getTxFee().value + : 0L; + // A BSQ Swap trade fee is paid in full by the BTC buyer (selling BSQ). + // The transferred BSQ (payout) is reduced by the peer's trade fee. + var takerFeeInBsq = !isMyOffer && bsqSwapTrade.getOffer().getDirection().equals(SELL) + ? bsqSwapTrade.getTakerFeeAsLong() + : 0L; TradeInfo tradeInfo = new TradeInfoV1Builder() .withOffer(offerInfo) .withTradeId(bsqSwapTrade.getId()) @@ -133,24 +153,28 @@ public static TradeInfo toTradeInfo(BsqSwapTrade bsqSwapTrade, .withDate(bsqSwapTrade.getDate().getTime()) .withRole(role == null ? "" : role) .withIsCurrencyForTakerFeeBtc(false) // BSQ Swap fees always paid in BSQ. - .withTxFeeAsLong(bsqSwapTrade.getTxFee().value) - .withTakerFeeAsLong(bsqSwapTrade.getTakerFeeAsLong()) - // N/A: .withTakerFeeTxId(""), .withDepositTxId(""), .withPayoutTxId("") + .withTxFeeAsLong(txFeeInBtc) + .withTakerFeeAsLong(takerFeeInBsq) + // N/A for bsq-swaps: .withTakerFeeTxId(""), .withDepositTxId(""), .withPayoutTxId("") .withTradeAmountAsLong(bsqSwapTrade.getAmountAsLong()) .withTradePrice(bsqSwapTrade.getPrice().getValue()) .withTradeVolume(bsqSwapTrade.getVolume() == null ? 0 : bsqSwapTrade.getVolume().getValue()) .withTradingPeerNodeAddress(requireNonNull(bsqSwapTrade.getTradingPeerNodeAddress().getFullAddress())) .withState(bsqSwapTrade.getTradeState().name()) .withPhase(bsqSwapTrade.getTradePhase().name()) - // N/A: .withTradePeriodState(""), .withIsDepositPublished(false), .withIsDepositConfirmed(false) - // N/A: .withIsFiatSent(false), .withIsFiatReceived(false), .withIsPayoutPublished(false) - // N/A: .withIsWithdrawn(false), .withContractAsJson(""), .withContract(null) + // N/A for bsq-swaps: .withTradePeriodState(""), .withIsDepositPublished(false), .withIsDepositConfirmed(false) + // N/A for bsq-swaps: .withIsFiatSent(false), .withIsFiatReceived(false), .withIsPayoutPublished(false) + // N/A for bsq-swaps: .withIsWithdrawn(false), .withContractAsJson(""), .withContract(null) + .withClosingStatus(closingStatus) .build(); tradeInfo.bsqSwapTradeInfo = toBsqSwapTradeInfo(bsqSwapTrade, isMyOffer, numConfirmations); return tradeInfo; } - private static TradeInfo toTradeInfo(Trade trade, String role, boolean isMyOffer) { + private static TradeInfo toTradeInfo(Trade trade, + String role, + boolean isMyOffer, + String closingStatus) { ContractInfo contractInfo; if (trade.getContract() != null) { Contract contract = trade.getContract(); @@ -198,6 +222,7 @@ private static TradeInfo toTradeInfo(Trade trade, String role, boolean isMyOffer .withIsWithdrawn(trade.isWithdrawn()) .withContractAsJson(trade.getContractAsJson()) .withContract(contractInfo) + .withClosingStatus(closingStatus) .build(); } @@ -232,8 +257,8 @@ public bisq.proto.grpc.TradeInfo toProtoMessage() { .setIsFiatSent(isFiatSent) .setIsFiatReceived(isFiatReceived) .setIsPayoutPublished(isPayoutPublished) - .setIsWithdrawn(isWithdrawn); - + .setIsWithdrawn(isWithdrawn) + .setClosingStatus(closingStatus); if (offer.isBsqSwapOffer()) { protoBuilder.setBsqSwapTradeInfo(bsqSwapTradeInfo.toProtoMessage()); } else { @@ -272,6 +297,7 @@ public static TradeInfo fromProto(bisq.proto.grpc.TradeInfo proto) { .withIsWithdrawn(proto.getIsWithdrawn()) .withContractAsJson(proto.getContractAsJson()) .withContract((ContractInfo.fromProto(proto.getContract()))) + .withClosingStatus(proto.getClosingStatus()) .build(); if (proto.getOffer().getIsBsqSwapOffer()) @@ -310,6 +336,7 @@ public String toString() { ", contractAsJson=" + contractAsJson + "\n" + ", contract=" + contract + "\n" + ", bsqSwapTradeInfo=" + bsqSwapTradeInfo + "\n" + + ", closingStatus=" + closingStatus + "\n" + '}'; } } diff --git a/core/src/main/java/bisq/core/api/model/builder/BsqSwapTradeInfoBuilder.java b/core/src/main/java/bisq/core/api/model/builder/BsqSwapTradeInfoBuilder.java index 3ae01d4ea8c..0df52ee9f7b 100644 --- a/core/src/main/java/bisq/core/api/model/builder/BsqSwapTradeInfoBuilder.java +++ b/core/src/main/java/bisq/core/api/model/builder/BsqSwapTradeInfoBuilder.java @@ -45,6 +45,8 @@ public final class BsqSwapTradeInfoBuilder { private String takerBtcAddress; private long numConfirmations; private String errorMessage; + private long payout; + private long swapPeerPayout; public BsqSwapTradeInfoBuilder withTxId(String txId) { this.txId = txId; @@ -106,6 +108,16 @@ public BsqSwapTradeInfoBuilder withErrorMessage(String errorMessage) { return this; } + public BsqSwapTradeInfoBuilder withPayout(long payout) { + this.payout = payout; + return this; + } + + public BsqSwapTradeInfoBuilder withSwapPeerPayout(long swapPeerPayout) { + this.swapPeerPayout = swapPeerPayout; + return this; + } + public BsqSwapTradeInfo build() { return new BsqSwapTradeInfo(this); } diff --git a/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java b/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java index 2bff2254a85..3fcc29eff8c 100644 --- a/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java +++ b/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java @@ -58,6 +58,7 @@ public final class TradeInfoV1Builder { private boolean isWithdrawn; private String contractAsJson; private ContractInfo contract; + private String closingStatus; public TradeInfoV1Builder withOffer(OfferInfo offer) { this.offer = offer; @@ -189,6 +190,12 @@ public TradeInfoV1Builder withContract(ContractInfo contract) { return this; } + + public TradeInfoV1Builder withClosingStatus(String closingStatus) { + this.closingStatus = closingStatus; + return this; + } + public TradeInfo build() { return new TradeInfo(this); } diff --git a/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java b/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java index cbe8e5b88a0..23a6059a062 100644 --- a/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java +++ b/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java @@ -53,6 +53,8 @@ import static bisq.core.util.FormattingUtils.formatToPercentWithSymbol; import static bisq.core.util.VolumeUtil.formatVolume; import static bisq.core.util.VolumeUtil.formatVolumeWithCode; +import static org.bitcoinj.core.TransactionConfidence.ConfidenceType.BUILDING; +import static org.bitcoinj.core.TransactionConfidence.ConfidenceType.PENDING; @Slf4j @Singleton @@ -212,11 +214,13 @@ public String getStateAsString(Tradable tradable) { } else if (isBsqSwapTrade(tradable)) { String txId = castToBsqSwapTrade(tradable).getTxId(); TransactionConfidence confidence = bsqWalletService.getConfidenceForTxId(txId); - if (confidence != null && confidence.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) { + if (confidence != null && confidence.getConfidenceType() == BUILDING) { return Res.get("confidence.confirmed.short"); + } else if (confidence != null && confidence.getConfidenceType() == PENDING) { + return Res.get("confidence.pending"); } else { log.warn("Unexpected confidence in a BSQ swap trade which has been moved to closed trades. " + - "This could happen at a wallet SPV resycn or a reorg. confidence={} tradeID={}", + "This could happen at a wallet SPV resync or a reorg. confidence={} tradeID={}", confidence, tradable.getId()); } } diff --git a/core/src/main/java/bisq/core/trade/ClosedTradableManager.java b/core/src/main/java/bisq/core/trade/ClosedTradableManager.java index 0eb00bf5f8f..bc586807ce1 100644 --- a/core/src/main/java/bisq/core/trade/ClosedTradableManager.java +++ b/core/src/main/java/bisq/core/trade/ClosedTradableManager.java @@ -21,6 +21,7 @@ import bisq.core.monetary.Price; import bisq.core.monetary.Volume; import bisq.core.offer.Offer; +import bisq.core.offer.OpenOffer; import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.bisq_v1.CleanupMailboxMessagesService; import bisq.core.trade.bisq_v1.DumpDelayedPayoutTx; @@ -57,6 +58,7 @@ import lombok.extern.slf4j.Slf4j; +import static bisq.core.offer.OpenOffer.State.CANCELED; import static bisq.core.trade.ClosedTradableUtil.castToTrade; import static bisq.core.trade.ClosedTradableUtil.castToTradeModel; import static bisq.core.trade.ClosedTradableUtil.isBsqSwapTrade; @@ -149,6 +151,13 @@ public List getClosedTrades() { .collect(Collectors.toList())); } + public List getCanceledOpenOffers() { + return ImmutableList.copyOf(getObservableList().stream() + .filter(e -> (e instanceof OpenOffer) && ((OpenOffer) e).getState().equals(CANCELED)) + .map(e -> (OpenOffer) e) + .collect(Collectors.toList())); + } + public Optional getTradableById(String id) { return closedTradables.stream().filter(e -> e.getId().equals(id)).findFirst(); } diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index e68410c9c30..22586f991d1 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -108,6 +108,7 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -870,6 +871,12 @@ public Optional getTradeById(String tradeId) { .map(tradeModel -> (Trade) tradeModel); } + public List getTrades() { + return getObservableList().stream() + .filter(t -> !t.hasFailed()) + .collect(Collectors.toList()); + } + private void removeTrade(Trade trade) { if (tradableList.remove(trade)) { requestPersistence(); diff --git a/core/src/main/java/bisq/core/trade/bisq_v1/FailedTradesManager.java b/core/src/main/java/bisq/core/trade/bisq_v1/FailedTradesManager.java index 2ab852f9728..2a5a41563e9 100644 --- a/core/src/main/java/bisq/core/trade/bisq_v1/FailedTradesManager.java +++ b/core/src/main/java/bisq/core/trade/bisq_v1/FailedTradesManager.java @@ -40,6 +40,7 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.slf4j.Logger; @@ -122,6 +123,10 @@ public ObservableList getObservableList() { return failedTrades.getObservableList(); } + public List getTrades() { + return getObservableList().stream().collect(Collectors.toList()); + } + public Optional getTradeById(String id) { return failedTrades.stream().filter(e -> e.getId().equals(id)).findFirst(); } diff --git a/core/src/main/resources/help/gettrades-help.txt b/core/src/main/resources/help/gettrades-help.txt new file mode 100644 index 00000000000..d23608d8091 --- /dev/null +++ b/core/src/main/resources/help/gettrades-help.txt @@ -0,0 +1,33 @@ +gettrades + +NAME +---- +gettrades - get open, closed, or failed trades + +SYNOPSIS +-------- +gettrades + --category= + +DESCRIPTION +----------- +Displays list of all currently open, closed, or failed trades. + +OPTIONS +------- +--category + The category of trade summaries: open (pending), closed (historical), and failed. + The default category option value is open. + +EXAMPLES +-------- +To see summaries of all open (pending) trades: +$ ./bisq-cli --password=xyz --port=9998 gettrades +OR +$ ./bisq-cli --password=xyz --port=9998 gettrades --category=open + +To see summaries of all closed (historical) trades: +$ ./bisq-cli --password=xyz --port=9998 gettrades ---category=closed + +To see summaries of all failed trades: +$ ./bisq-cli --password=xyz --port=9998 gettrades ---category=failed diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 840cca5aad0..5f675eb87cb 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -3232,6 +3232,7 @@ confidence.seen=Seen by {0} peer(s) / 0 confirmations confidence.confirmed=Confirmed in {0} block(s) confidence.invalid=Transaction is invalid confidence.confirmed.short=Confirmed +confidence.pending=Pending peerInfo.title=Peer info peerInfo.nrOfTrades=Number of completed trades diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java index dbcb4c6c51d..9331e154cef 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -18,7 +18,9 @@ package bisq.daemon.grpc; import bisq.core.api.CoreApi; +import bisq.core.api.model.CanceledTradeInfo; import bisq.core.api.model.TradeInfo; +import bisq.core.offer.OpenOffer; import bisq.core.trade.model.TradeModel; import bisq.core.trade.model.bisq_v1.Trade; import bisq.core.trade.model.bsq_swap.BsqSwapTrade; @@ -33,6 +35,8 @@ import bisq.proto.grpc.FailTradeRequest; import bisq.proto.grpc.GetTradeReply; import bisq.proto.grpc.GetTradeRequest; +import bisq.proto.grpc.GetTradesReply; +import bisq.proto.grpc.GetTradesRequest; import bisq.proto.grpc.TakeOfferReply; import bisq.proto.grpc.TakeOfferRequest; import bisq.proto.grpc.UnFailTradeReply; @@ -45,15 +49,22 @@ import javax.inject.Inject; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import static bisq.core.api.model.TradeInfo.toNewTradeInfo; import static bisq.core.api.model.TradeInfo.toTradeInfo; +import static bisq.core.trade.model.bsq_swap.BsqSwapTrade.State.COMPLETED; import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; +import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED; +import static bisq.proto.grpc.GetTradesRequest.Category.OPEN; import static bisq.proto.grpc.TradesGrpc.*; +import static java.util.Comparator.comparing; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; @@ -129,6 +140,24 @@ public void getTrade(GetTradeRequest req, } } + @Override + public void getTrades(GetTradesRequest req, + StreamObserver responseObserver) { + try { + var category = req.getCategory(); + var trades = category.equals(OPEN) + ? coreApi.getOpenTrades() + : coreApi.getTradeHistory(category); + var reply = buildGetTradesReply(trades, category); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalArgumentException cause) { + exceptionHandler.handleExceptionAsWarning(log, "getTrades", cause, responseObserver); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + @Override public void confirmPaymentStarted(ConfirmPaymentStartedRequest req, StreamObserver responseObserver) { @@ -218,6 +247,7 @@ final Optional rateMeteringInterceptor() { .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); @@ -245,10 +275,14 @@ private GetTradeReply buildGetTradeReply(BsqSwapTrade bsqSwapTrade) { boolean wasMyOffer = wasMyOffer(bsqSwapTrade); String role = getMyRole(bsqSwapTrade); var numConfirmations = coreApi.getTransactionConfirmations(bsqSwapTrade.getTxId()); + var closingStatus = bsqSwapTrade.getTradeState().equals(COMPLETED) + ? coreApi.getClosedTradeStateAsString(bsqSwapTrade) + : "Pending"; var tradeInfo = toTradeInfo(bsqSwapTrade, role, wasMyOffer, - numConfirmations); + numConfirmations, + closingStatus); return GetTradeReply.newBuilder() .setTrade(tradeInfo.toProtoMessage()) .build(); @@ -257,8 +291,60 @@ private GetTradeReply buildGetTradeReply(BsqSwapTrade bsqSwapTrade) { private GetTradeReply buildGetTradeReply(Trade trade) { boolean wasMyOffer = wasMyOffer(trade); String role = getMyRole(trade); + var closingStatus = trade.isCompleted() + ? coreApi.getClosedTradeStateAsString(trade) + : "Pending"; return GetTradeReply.newBuilder() - .setTrade(toTradeInfo(trade, role, wasMyOffer).toProtoMessage()) + .setTrade(toTradeInfo(trade, + role, + wasMyOffer, + closingStatus).toProtoMessage()) + .build(); + } + + + private GetTradesReply buildGetTradesReply(List trades, GetTradesRequest.Category category) { + // Build an unsorted List, starting with + // all pending, or all completed BsqSwap and v1 trades. + List unsortedTrades = trades.stream() + .map(tradeModel -> { + var role = coreApi.getTradeRole(tradeModel); + var isMyOffer = coreApi.isMyOffer(tradeModel.getOffer()); + var isBsqSwapTrade = tradeModel instanceof BsqSwapTrade; + var numConfirmations = isBsqSwapTrade + ? coreApi.getTransactionConfirmations(((BsqSwapTrade) tradeModel).getTxId()) + : 0; + var closingStatus = category.equals(OPEN) + ? "Pending" + : coreApi.getClosedTradeStateAsString(tradeModel); + return isBsqSwapTrade + ? toTradeInfo((BsqSwapTrade) tradeModel, role, isMyOffer, numConfirmations, closingStatus) + : toTradeInfo(tradeModel, role, isMyOffer, closingStatus); + }) + .collect(Collectors.toList()); + + // If closed trades were requested, add any canceled + // OpenOffers (canceled trades) to the unsorted List. + Optional> canceledOpenOffers = category.equals(CLOSED) + ? Optional.of(coreApi.getCanceledOpenOffers()) + : Optional.empty(); + List canceledTrades = new ArrayList<>(); + canceledOpenOffers.ifPresent(openOffers -> canceledTrades.addAll( + openOffers.stream() + .map(CanceledTradeInfo::toCanceledTradeInfo) + .collect(Collectors.toList()) + )); + unsortedTrades.addAll(canceledTrades); + + // Sort the cumulative List by date before sending it to the client. + List sortedTrades = unsortedTrades.stream() + .sorted(comparing(TradeInfo::getDate)) + .collect(Collectors.toList()); + + return GetTradesReply.newBuilder() + .addAllTrades(sortedTrades.stream() + .map(TradeInfo::toProtoMessage) + .collect(Collectors.toList())) .build(); } @@ -267,8 +353,6 @@ private boolean wasMyOffer(TradeModel tradeModel) { } private String getMyRole(TradeModel tradeModel) { - return tradeModel.getOffer().isBsqSwapOffer() - ? coreApi.getBsqSwapTradeRole((BsqSwapTrade) tradeModel) - : coreApi.getTradeRole(tradeModel.getId()); + return coreApi.getTradeRole(tradeModel); } } diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index bb4504c74c9..0a0c4921f33 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -388,6 +388,8 @@ message StopReply { service Trades { rpc GetTrade (GetTradeRequest) returns (GetTradeReply) { } + rpc GetTrades (GetTradesRequest) returns (GetTradesReply) { + } rpc TakeOffer (TakeOfferRequest) returns (TakeOfferReply) { } rpc ConfirmPaymentStarted (ConfirmPaymentStartedRequest) returns (ConfirmPaymentStartedReply) { @@ -437,6 +439,19 @@ message GetTradeReply { TradeInfo trade = 1; } +message GetTradesRequest { + enum Category { + OPEN = 0; + CLOSED = 1; + FAILED = 2; + } + Category category = 1; +} + +message GetTradesReply { + repeated TradeInfo trades = 1; +} + message CloseTradeRequest { string tradeId = 1; } @@ -495,8 +510,14 @@ message TradeInfo { string contractAsJson = 24; ContractInfo contract = 25; uint64 tradeVolume = 26; + // Optional Bisq v2+ trade protocol fields. BsqSwapTradeInfo bsqSwapTradeInfo = 28; + + // Needed by open/closed/failed trade list items. + string closingStatus = 29; + + // TODO? Field for displaying correct precision per coin type, e.g., int32 coinPrecision = 32; } message ContractInfo { @@ -528,6 +549,8 @@ message BsqSwapTradeInfo { string takerBtcAddress = 10; uint64 numConfirmations = 11; string errorMessage = 12; + uint64 payout = 13; + uint64 swapPeerPayout = 14; } message PaymentAccountPayloadInfo {