Merge pull request #5976 from ghubstan/1-gettrades

Add API 'gettrades' method
This commit is contained in:
Christoph Atteneder 2022-02-01 11:10:45 +01:00 committed by GitHub
commit fcb13ed772
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1031 additions and 226 deletions

View File

@ -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.bobdaemon;
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 abstract class AbstractOfferTest extends MethodTest {
protected final Function<List<OfferInfo>, String> toOffersTable = (offers) ->
new TableBuilder(OFFER_TBL, offers).build().toString();
protected OfferInfo getAvailableBsqSwapOffer(GrpcClient client,
OfferDirection direction,
boolean checkForLoggedExceptions) {
List<OfferInfo> 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,

View File

@ -264,4 +264,18 @@ public class AbstractTradeTest extends AbstractOfferTest {
out.println("Bob's CLI 'gettrade' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrade", "--trade-id=" + tradeId});
}
protected static void runCliGetOpenTrades() {
out.println("Alice's CLI 'gettrades --category=open' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9998", "gettrades", "--category=open"});
out.println("Bob's CLI 'gettrades --category=open' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrades", "--category=open"});
}
protected static void runCliGetClosedTrades() {
out.println("Alice's CLI 'gettrades --category=closed' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9998", "gettrades", "--category=closed"});
out.println("Bob's CLI 'gettrades --category=closed' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrades", "--category=closed"});
}
}

View File

@ -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 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.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.assertTrue;
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 @@ import bisq.cli.GrpcClient;
@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 class BsqSwapTradeTest extends AbstractTradeTest {
@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 class BsqSwapTradeTest extends AbstractTradeTest {
@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 class BsqSwapTradeTest extends AbstractTradeTest {
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 class BsqSwapTradeTest extends AbstractTradeTest {
log.debug("BsqSwap Trade at COMPLETION:\n{}", toTradeDetailTable.apply(swapTrade));
assertEquals(COMPLETED.name(), swapTrade.getState());
runCliGetTrade(tradeId);
runCliGetClosedTrades();
}
@Test
@ -151,6 +150,8 @@ public class BsqSwapTradeTest extends AbstractTradeTest {
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 class BsqSwapTradeTest extends AbstractTradeTest {
log.debug("Bob's After Trade Balance:\n{}", formatBalancesTbls(bobsBalances));
}
private OfferInfo getAvailableBsqSwapOffer(GrpcClient client) {
List<OfferInfo> 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) {

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}
}
}

View File

@ -133,7 +133,7 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
@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 class TakeBuyBTCOfferTest extends AbstractTradeTest {
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);
}

View File

@ -152,7 +152,7 @@ public class TakeBuyXMROfferTest extends AbstractTradeTest {
@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);

View File

@ -31,7 +31,7 @@ import static java.lang.System.getenv;
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 class LongRunningBsqSwapTest extends AbstractOfferTest {
@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 class LongRunningBsqSwapTest extends AbstractOfferTest {
test.testGetBalancesBeforeTrade();
test.testAliceCreateBsqSwapBuyOffer();
test.testAliceCreateBsqSwapBuyBtcOffer();
genBtcBlocksThenWait(1, 8_000);
test.testBobTakesBsqSwapOffer();

View File

@ -71,7 +71,7 @@ public class LongRunningTradesTest extends AbstractTradeTest {
test.testTakeAlicesBuyOffer(testInfo);
test.testAlicesConfirmPaymentStarted(testInfo);
test.testBobsConfirmPaymentReceived(testInfo);
test.testKeepFunds(testInfo);
test.testCloseTrade(testInfo);
}
public void testTakeSellBTCOffer(final TestInfo testInfo) {

View File

@ -29,7 +29,8 @@ import org.junit.jupiter.api.TestMethodOrder;
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 class TradeTest extends AbstractTradeTest {
test.testTakeAlicesBuyOffer(testInfo);
test.testAlicesConfirmPaymentStarted(testInfo);
test.testBobsConfirmPaymentReceived(testInfo);
test.testKeepFunds(testInfo);
test.testCloseTrade(testInfo);
}
@Test
@ -108,7 +109,7 @@ public class TradeTest extends AbstractTradeTest {
test.testTakeAlicesSellBTCForXMROffer(testInfo);
test.testBobsConfirmPaymentStarted(testInfo);
test.testAlicesConfirmPaymentReceived(testInfo);
test.testKeepFunds(testInfo);
test.testCloseTrade(testInfo);
}
@Test
@ -124,16 +125,26 @@ public class TradeTest extends AbstractTradeTest {
@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);

View File

@ -47,6 +47,8 @@ import static bisq.cli.Method.*;
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.GetBalanceOptionParser;
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 class CliMain {
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 @@ public class CliMain {
stream.format(rowFormat, gettrade.name(), "--trade-id=<trade-id> \\", "Get trade summary or full contract");
stream.format(rowFormat, "", "[--show-contract=<true|false>]", "");
stream.println();
stream.format(rowFormat, gettrades.name(), "[--category=<open|closed|failed>]", "Get open (default), closed, or failed trades");
stream.println();
stream.format(rowFormat, confirmpaymentstarted.name(), "--trade-id=<trade-id>", "Confirm payment started");
stream.println();
stream.format(rowFormat, confirmpaymentreceived.name(), "--trade-id=<trade-id>", "Confirm payment received");

View File

@ -22,6 +22,7 @@ import bisq.proto.grpc.BalancesInfo;
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 final class GrpcClient {
return tradesServiceRequest.getTrade(tradeId);
}
public List<TradeInfo> getOpenTrades() {
return tradesServiceRequest.getOpenTrades();
}
public List<TradeInfo> getTradeHistory(GetTradesRequest.Category category) {
return tradesServiceRequest.getTradeHistory(category);
}
public void confirmPaymentStarted(String tradeId) {
tradesServiceRequest.confirmPaymentStarted(tradeId);
}

View File

@ -42,6 +42,7 @@ public enum Method {
getpaymentaccts,
getpaymentmethods,
gettrade,
gettrades,
failtrade,
unfailtrade,
gettransaction,

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import bisq.proto.grpc.GetTradesRequest;
import joptsimple.OptionSpec;
import java.util.function.Predicate;
import static bisq.cli.opts.OptLabel.OPT_CATEGORY;
import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED;
import static bisq.proto.grpc.GetTradesRequest.Category.FAILED;
import static bisq.proto.grpc.GetTradesRequest.Category.OPEN;
import static java.util.Arrays.stream;
public class GetTradesOptionParser extends AbstractMethodOptionParser implements MethodOpts {
// Map valid CLI option values to gRPC request parameters.
private enum CATEGORY {
// Lower case enum fits CLI method and parameter style.
open(OPEN),
closed(CLOSED),
failed(FAILED);
private final GetTradesRequest.Category grpcRequestCategory;
CATEGORY(GetTradesRequest.Category grpcRequestCategory) {
this.grpcRequestCategory = grpcRequestCategory;
}
}
final OptionSpec<String> categoryOpt = parser.accepts(OPT_CATEGORY,
"category of trades (open|closed|failed)")
.withRequiredArg()
.defaultsTo(CATEGORY.open.name());
private final Predicate<String> isValidCategory = (c) ->
stream(CATEGORY.values()).anyMatch(v -> v.name().equalsIgnoreCase(c));
public GetTradesOptionParser(String[] args) {
super(args);
}
public GetTradesOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (options.has(categoryOpt)) {
String category = options.valueOf(categoryOpt);
if (category.isEmpty())
throw new IllegalArgumentException("no category (open|closed|failed) specified");
if (!isValidCategory.test(category))
throw new IllegalArgumentException("category must be open|closed|failed");
}
return this;
}
public GetTradesRequest.Category getCategory() {
String cliOpt = options.valueOf(categoryOpt);
return CATEGORY.valueOf(cliOpt).grpcRequestCategory;
}
}

View File

@ -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";

View File

@ -22,12 +22,18 @@ import bisq.proto.grpc.ConfirmPaymentReceivedRequest;
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 class TradesServiceRequest {
return grpcStubs.tradesService.getTrade(request).getTrade();
}
public List<TradeInfo> getOpenTrades() {
var request = GetTradesRequest.newBuilder()
.build();
return grpcStubs.tradesService.getTrades(request).getTradesList();
}
public List<TradeInfo> getTradeHistory(GetTradesRequest.Category category) {
if (!category.equals(CLOSED) && !category.equals(FAILED))
throw new IllegalStateException("unrecognized gettrades category parameter " + category.name());
var request = GetTradesRequest.newBuilder()
.setCategory(category)
.build();
return grpcStubs.tradesService.getTrades(request).getTradesList();
}
public void confirmPaymentStarted(String tradeId) {
var request = ConfirmPaymentStartedRequest.newBuilder()
.setTradeId(tradeId)

View File

@ -26,7 +26,6 @@ import java.math.BigDecimal;
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 javax.annotation.Nullable;
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<String> colOfferType;
@Nullable
protected final Column<String> colStatusDescription;
protected final Column<String> 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 @@ abstract class AbstractTradeListBuilder extends AbstractTableBuilder {
private final Supplier<Boolean> isTradeDetailTblBuilder = () -> tableType.equals(TRADE_DETAIL_TBL);
protected final Predicate<TradeInfo> isFiatTrade = (t) -> isFiatOffer.test(t.getOffer());
protected final Predicate<TradeInfo> isBsqSwapTrade = (t) -> t.getOffer().getIsBsqSwapOffer();
protected final Predicate<TradeInfo> isMyOffer = (t) -> t.getOffer().getIsMyOffer();
protected final Predicate<TradeInfo> isTaker = (t) -> t.getRole().toLowerCase().contains("taker");
protected final Predicate<TradeInfo> isSellOffer = (t) -> t.getOffer().getDirection().equals(SELL.name());
protected final Predicate<TradeInfo> isBtcSeller = (t) -> (isMyOffer.test(t) && isSellOffer.test(t))
|| (!isMyOffer.test(t) && !isSellOffer.test(t));
protected final Predicate<TradeInfo> isTradeFeeBtc = (t) -> isMyOffer.test(t)
? t.getOffer().getIsCurrencyForMakerFeeBtc()
: t.getIsCurrencyForTakerFeeBtc();
// Column Value Functions
@ -206,12 +214,19 @@ abstract class AbstractTradeListBuilder extends AbstractTableBuilder {
? formatToPercent(t.getOffer().getMarketPriceMargin())
: "N/A";
protected final Function<TradeInfo, Long> toMyMinerTxFee = (t) ->
isTaker.test(t)
protected final Function<TradeInfo, Long> 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<TradeInfo, Boolean, Long> toTradeFeeBsq = (t, isMyOffer) -> {
protected final Function<TradeInfo, Long> toTradeFeeBsq = (t) -> {
var isMyOffer = t.getOffer().getIsMyOffer();
if (isMyOffer) {
return t.getOffer().getIsCurrencyForMakerFeeBtc()
? 0L // Maker paid BTC fee, return 0.
@ -223,7 +238,8 @@ abstract class AbstractTradeListBuilder extends AbstractTableBuilder {
}
};
protected final BiFunction<TradeInfo, Boolean, Long> toTradeFeeBtc = (t, isMyOffer) -> {
protected final Function<TradeInfo, Long> toTradeFeeBtc = (t) -> {
var isMyOffer = t.getOffer().getIsMyOffer();
if (isMyOffer) {
return t.getOffer().getIsCurrencyForMakerFeeBtc()
? t.getOffer().getMakerFee()

View File

@ -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<String> colTradeId;
@Nullable
protected final Column<Long> colCreateDate;
@Nullable
protected final Column<String> colMarket;
@Nullable
protected final MixedPriceColumn colMixedPrice;
@Nullable
protected final Column<String> colPriceDeviation;
@Nullable
protected final Column<Long> colAmountInBtc;
@Nullable
protected final MixedVolumeColumn colMixedAmount;
@Nullable
protected final Column<String> colCurrency;
@Nullable
protected final MixedTradeFeeColumn colMixedTradeFee;
@Nullable
protected final Column<Long> colBuyerDeposit;
@Nullable
protected final Column<Long> colSellerDeposit;
@Nullable
protected final Column<String> colOfferType;
@Nullable
protected final Column<String> colStatus;
protected final Column<Long> 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<TradeInfo> isFiatTrade = (t) -> isFiatOffer.test(t.getOffer());
protected final Predicate<TradeInfo> isTaker = (t) -> t.getRole().toLowerCase().contains("taker");
protected final Function<TradeInfo, String> toPaymentCurrencyCode = (t) ->
isFiatTrade.test(t)
? t.getOffer().getCounterCurrencyCode()
: t.getOffer().getBaseCurrencyCode();
protected final Function<TradeInfo, Long> toAmount = (t) ->
isFiatTrade.test(t)
? t.getTradeAmountAsLong()
: t.getTradeVolume();
protected final Function<TradeInfo, Long> toMinerTxFee = (t) ->
isTaker.test(t)
? t.getTxFeeAsLong()
: t.getOffer().getTxFee();
protected final Function<TradeInfo, Long> toMakerTakerFee = (t) ->
isTaker.test(t)
? t.getTakerFeeAsLong()
: t.getOffer().getMakerFee();
protected final Function<TradeInfo, Long> toTradeCost = (t) ->
isFiatTrade.test(t)
? t.getTradeVolume()
: t.getTradeAmountAsLong();
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.table.builder;
import java.util.List;
import static bisq.cli.table.builder.TableType.CLOSED_TRADES_TBL;
import bisq.cli.table.Table;
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());
});
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 @@ import bisq.cli.table.column.MixedPriceColumn;
class FailedTradeTableBuilder extends AbstractTradeListBuilder {
FailedTradeTableBuilder(List<?> protos) {
super(FAILED_TRADE_TBL, protos);
super(FAILED_TRADES_TBL, protos);
}
public Table build() {
@ -29,7 +46,7 @@ class FailedTradeTableBuilder extends AbstractTradeListBuilder {
colCurrency,
colOfferType,
colRole,
colStatusDescription);
colClosingStatus);
}
private void populateColumns() {
@ -43,7 +60,7 @@ class FailedTradeTableBuilder extends AbstractTradeListBuilder {
colCurrency.addRow(toPaymentCurrencyCode.apply(t));
colOfferType.addRow(toOfferType.apply(t));
colRole.addRow(t.getRole());
colStatusDescription.addRow("Failed");
colClosingStatus.addRow("Failed");
});
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 @@ import bisq.cli.table.column.MixedPriceColumn;
class OpenTradeTableBuilder extends AbstractTradeListBuilder {
OpenTradeTableBuilder(List<?> protos) {
super(OPEN_TRADE_TBL, protos);
super(OPEN_TRADES_TBL, protos);
}
public Table build() {

View File

@ -49,14 +49,14 @@ public class TableBuilder extends AbstractTableBuilder {
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:

View File

@ -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";

View File

@ -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

View File

@ -32,9 +32,9 @@ import lombok.extern.slf4j.Slf4j;
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 @@ class TradeTableColumnSupplier {
}
private final Supplier<Boolean> isTradeDetailTblBuilder = () -> getTableType().equals(TRADE_DETAIL_TBL);
private final Supplier<Boolean> isOpenTradeTblBuilder = () -> getTableType().equals(OPEN_TRADE_TBL);
private final Supplier<Boolean> isClosedTradeTblBuilder = () -> getTableType().equals(CLOSED_TRADE_TBL);
private final Supplier<Boolean> isFailedTradeTblBuilder = () -> getTableType().equals(FAILED_TRADE_TBL);
private final Supplier<Boolean> isOpenTradeTblBuilder = () -> getTableType().equals(OPEN_TRADES_TBL);
private final Supplier<Boolean> isClosedTradeTblBuilder = () -> getTableType().equals(CLOSED_TRADES_TBL);
private final Supplier<Boolean> isFailedTradeTblBuilder = () -> getTableType().equals(FAILED_TRADES_TBL);
private final Supplier<TradeInfo> firstRow = () -> getTrades().get(0);
private final Predicate<OfferInfo> isFiatOffer = (o) -> o.getBaseCurrencyCode().equals("BTC");
private final Predicate<TradeInfo> isFiatTrade = (t) -> isFiatOffer.test(t.getOffer());

View File

@ -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<TradeInfo> openTrades;
private final List<TradeInfo> 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);
}
}

View File

@ -26,6 +26,7 @@ import bisq.core.offer.OpenOffer;
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.config.Config;
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 class CoreApi {
return coreTradesService.getTradeModel(tradeId);
}
public String getTradeRole(String tradeId) {
return coreTradesService.getTradeRole(tradeId);
public List<TradeModel> getOpenTrades() {
return coreTradesService.getOpenTrades();
}
public String getBsqSwapTradeRole(BsqSwapTrade bsqSwapTrade) {
return coreTradesService.getBsqSwapTradeRole(bsqSwapTrade);
public List<TradeModel> getTradeHistory(GetTradesRequest.Category category) {
return coreTradesService.getTradeHistory(category);
}
public String getTradeRole(TradeModel tradeModel) {
return coreTradesService.getTradeRole(tradeModel);
}
public void failTrade(String tradeId) {
@ -343,6 +350,14 @@ public class CoreApi {
coreTradesService.unFailTrade(tradeId);
}
public List<OpenOffer> getCanceledOpenOffers() {
return coreTradesService.getCanceledOpenOffers();
}
public String getClosedTradeStateAsString(Tradable tradable) {
return coreTradesService.getClosedTradeStateAsString(tradable);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Wallets
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -21,13 +21,16 @@ import bisq.core.btc.model.AddressEntry;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferUtil;
import bisq.core.offer.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.core.util.validation.BtcAddressValidator;
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 @@ class CoreTradesService {
CoreWalletsService coreWalletsService,
BtcWalletService btcWalletService,
OfferUtil offerUtil,
BsqSwapTradeManager bsqSwapTradeManager,
ClosedTradableManager closedTradableManager,
ClosedTradableFormatter closedTradableFormatter,
FailedTradesManager failedTradesManager,
TakeOfferModel takeOfferModel,
BsqSwapTakeOfferModel bsqSwapTakeOfferModel,
@ -87,7 +99,9 @@ class CoreTradesService {
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 @@ class CoreTradesService {
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
}
String getBsqSwapTradeRole(BsqSwapTrade bsqSwapTrade) {
String getTradeRole(TradeModel tradeModel) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
return tradeUtil.getRole(bsqSwapTrade);
}
String getTradeRole(String tradeId) {
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 @@ class CoreTradesService {
));
}
List<TradeModel> getOpenTrades() {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
return tradeManager.getTrades().stream().collect(Collectors.toList());
}
List<TradeModel> 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=<open|closed|failed>'
// method currently in development.
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
@ -304,6 +337,14 @@ class CoreTradesService {
});
}
List<OpenOffer> getCanceledOpenOffers() {
return closedTradableManager.getCanceledOpenOffers();
}
String getClosedTradeStateAsString(Tradable tradable) {
return closedTradableFormatter.getStateAsString(tradable);
}
private Optional<Trade> getOpenTrade(String tradeId) {
return tradeManager.getTradeById(tradeId);
}

View File

@ -25,6 +25,9 @@ import bisq.common.Payload;
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 class BsqSwapTradeInfo implements Payload {
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 class BsqSwapTradeInfo implements Payload {
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 class BsqSwapTradeInfo implements Payload {
.withTakerBtcAddress(takerBtcAddress)
.withNumConfirmations(numConfirmations)
.withErrorMessage(trade.getErrorMessage())
.withPayout(protocolModel.getPayout())
.withSwapPeerPayout(protocolModel.getTradePeer().getPayout())
.build();
}
@ -101,6 +118,9 @@ public class BsqSwapTradeInfo implements Payload {
.setTakerBtcAddress(takerBtcAddress != null ? takerBtcAddress : "")
.setTakerBtcAddress(takerBtcAddress != null ? takerBtcAddress : "")
.setNumConfirmations(numConfirmations)
.setErrorMessage(errorMessage != null ? errorMessage : "")
.setPayout(payout)
.setSwapPeerPayout(swapPeerPayout)
.build();
}
@ -118,6 +138,8 @@ public class BsqSwapTradeInfo implements Payload {
.withTakerBtcAddress(proto.getTakerBtcAddress())
.withNumConfirmations(proto.getNumConfirmations())
.withErrorMessage(proto.getErrorMessage())
.withPayout(proto.getPayout())
.withSwapPeerPayout(proto.getSwapPeerPayout())
.build();
}
@ -136,6 +158,8 @@ public class BsqSwapTradeInfo implements Payload {
", takerBtcAddress='" + takerBtcAddress + '\'' +
", numConfirmations='" + numConfirmations + '\'' +
", errorMessage='" + errorMessage + '\'' +
", payout=" + payout +
", swapPeerPayout=" + swapPeerPayout +
'}';
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -32,6 +32,8 @@ import static bisq.core.api.model.BsqSwapTradeInfo.toBsqSwapTradeInfo;
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 class TradeInfo implements Payload {
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 @@ public class TradeInfo implements Payload {
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 class TradeInfo implements Payload {
.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 @@ public class TradeInfo implements Payload {
.withIsWithdrawn(trade.isWithdrawn())
.withContractAsJson(trade.getContractAsJson())
.withContract(contractInfo)
.withClosingStatus(closingStatus)
.build();
}
@ -232,8 +257,8 @@ public class TradeInfo implements Payload {
.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 class TradeInfo implements Payload {
.withIsWithdrawn(proto.getIsWithdrawn())
.withContractAsJson(proto.getContractAsJson())
.withContract((ContractInfo.fromProto(proto.getContract())))
.withClosingStatus(proto.getClosingStatus())
.build();
if (proto.getOffer().getIsBsqSwapOffer())
@ -310,6 +336,7 @@ public class TradeInfo implements Payload {
", contractAsJson=" + contractAsJson + "\n" +
", contract=" + contract + "\n" +
", bsqSwapTradeInfo=" + bsqSwapTradeInfo + "\n" +
", closingStatus=" + closingStatus + "\n" +
'}';
}
}

View File

@ -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 final class BsqSwapTradeInfoBuilder {
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);
}

View File

@ -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 final class TradeInfoV1Builder {
return this;
}
public TradeInfoV1Builder withClosingStatus(String closingStatus) {
this.closingStatus = closingStatus;
return this;
}
public TradeInfo build() {
return new TradeInfo(this);
}

View File

@ -53,6 +53,8 @@ import static bisq.core.util.FormattingUtils.formatPercentagePrice;
import static bisq.core.util.FormattingUtils.formatToPercentWithSymbol;
import static bisq.core.util.VolumeUtil.formatVolume;
import static bisq.core.util.VolumeUtil.formatVolumeWithCode;
import static org.bitcoinj.core.TransactionConfidence.ConfidenceType.BUILDING;
import static org.bitcoinj.core.TransactionConfidence.ConfidenceType.PENDING;
@Slf4j
@Singleton
@ -212,11 +214,13 @@ public class ClosedTradableFormatter {
} 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());
}
}

View File

@ -21,6 +21,7 @@ import bisq.core.btc.wallet.BsqWalletService;
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;
@ -60,6 +61,7 @@ import java.util.stream.Stream;
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;
@ -154,6 +156,13 @@ public class ClosedTradableManager implements PersistedDataHost {
.collect(Collectors.toList()));
}
public List<OpenOffer> 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<Tradable> getTradableById(String id) {
return closedTradables.stream().filter(e -> e.getId().equals(id)).findFirst();
}

View File

@ -108,6 +108,7 @@ import org.bouncycastle.crypto.params.KeyParameter;
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 class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
.map(tradeModel -> (Trade) tradeModel);
}
public List<Trade> getTrades() {
return getObservableList().stream()
.filter(t -> !t.hasFailed())
.collect(Collectors.toList());
}
private void removeTrade(Trade trade) {
if (tradableList.remove(trade)) {
requestPersistence();

View File

@ -40,6 +40,7 @@ import java.util.List;
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 class FailedTradesManager implements PersistedDataHost {
return failedTrades.getObservableList();
}
public List<Trade> getTrades() {
return getObservableList().stream().collect(Collectors.toList());
}
public Optional<Trade> getTradeById(String id) {
return failedTrades.stream().filter(e -> e.getId().equals(id)).findFirst();
}

View File

@ -0,0 +1,33 @@
gettrades
NAME
----
gettrades - get open, closed, or failed trades
SYNOPSIS
--------
gettrades
--category=<open|closed|failed>
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

View File

@ -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

View File

@ -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.FailTradeReply;
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 io.grpc.stub.StreamObserver;
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 @@ class GrpcTradesService extends TradesImplBase {
}
}
@Override
public void getTrades(GetTradesRequest req,
StreamObserver<GetTradesReply> 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<ConfirmPaymentStartedReply> responseObserver) {
@ -218,6 +247,7 @@ class GrpcTradesService extends TradesImplBase {
.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 @@ class GrpcTradesService extends TradesImplBase {
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 @@ class GrpcTradesService extends TradesImplBase {
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<TradeModel> trades, GetTradesRequest.Category category) {
// Build an unsorted List<TradeInfo>, starting with
// all pending, or all completed BsqSwap and v1 trades.
List<TradeInfo> 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<TradeInfo>.
Optional<List<OpenOffer>> canceledOpenOffers = category.equals(CLOSED)
? Optional.of(coreApi.getCanceledOpenOffers())
: Optional.empty();
List<TradeInfo> canceledTrades = new ArrayList<>();
canceledOpenOffers.ifPresent(openOffers -> canceledTrades.addAll(
openOffers.stream()
.map(CanceledTradeInfo::toCanceledTradeInfo)
.collect(Collectors.toList())
));
unsortedTrades.addAll(canceledTrades);
// Sort the cumulative List<TradeInfo> by date before sending it to the client.
List<TradeInfo> 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 @@ class GrpcTradesService extends TradesImplBase {
}
private String getMyRole(TradeModel tradeModel) {
return tradeModel.getOffer().isBsqSwapOffer()
? coreApi.getBsqSwapTradeRole((BsqSwapTrade) tradeModel)
: coreApi.getTradeRole(tradeModel.getId());
return coreApi.getTradeRole(tradeModel);
}
}

View File

@ -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 {