From 7690ffd9530b15f1d5dce91e9b674b09111def19 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 7 Jan 2022 19:11:34 -0300 Subject: [PATCH] Add API methods 'failtrade', 'unfailtrade' Prerequisite for next PR: Add API method 'gettrades' The `gettrades` method will show 'open', 'closed', and 'failed' trades. Users already needed to be able to fail and unfail trades for the same reasons they do in the UI. API test cases will need to be able to fail and unfail trades to check correct behavior of 'gettrades' method. Based on branch `rename-keepfunds2closetrade`. --- .../method/trade/AbstractTradeTest.java | 3 + .../method/trade/FailUnfailTradeTest.java | 143 ++++++++++++++++++ .../java/bisq/apitest/scenario/TradeTest.java | 11 ++ cli/src/main/java/bisq/cli/CliMain.java | 26 ++++ cli/src/main/java/bisq/cli/GrpcClient.java | 8 + cli/src/main/java/bisq/cli/Method.java | 2 + .../cli/request/TradesServiceRequest.java | 18 +++ core/src/main/java/bisq/core/api/CoreApi.java | 8 + .../java/bisq/core/api/CoreTradesService.java | 61 ++++++++ .../trade/bisq_v1/FailedTradesManager.java | 74 +++++++-- .../main/resources/help/failtrade-help.txt | 28 ++++ .../main/resources/help/unfailtrade-help.txt | 32 ++++ .../bisq/daemon/grpc/GrpcTradesService.java | 30 ++++ proto/src/main/proto/grpc.proto | 18 +++ 14 files changed, 452 insertions(+), 10 deletions(-) create mode 100644 apitest/src/test/java/bisq/apitest/method/trade/FailUnfailTradeTest.java create mode 100644 core/src/main/resources/help/failtrade-help.txt create mode 100644 core/src/main/resources/help/unfailtrade-help.txt 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 625bcc0ab18..2c1fb29d320 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java @@ -8,6 +8,8 @@ import org.slf4j.Logger; +import lombok.Getter; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.TestInfo; @@ -36,6 +38,7 @@ public class AbstractTradeTest extends AbstractOfferTest { public static final ExpectedProtocolStatus EXPECTED_PROTOCOL_STATUS = new ExpectedProtocolStatus(); // A Trade ID cache for use in @Test sequences. + @Getter protected static String tradeId; protected final Supplier maxTradeStateAndPhaseChecks = () -> isLongRunningTest ? 10 : 2; diff --git a/apitest/src/test/java/bisq/apitest/method/trade/FailUnfailTradeTest.java b/apitest/src/test/java/bisq/apitest/method/trade/FailUnfailTradeTest.java new file mode 100644 index 00000000000..5985ea81285 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/FailUnfailTradeTest.java @@ -0,0 +1,143 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.method.trade; + +import io.grpc.StatusRuntimeException; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.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.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + + + +import bisq.apitest.method.offer.AbstractOfferTest; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class FailUnfailTradeTest extends AbstractTradeTest { + + @BeforeAll + public static void setUp() { + AbstractOfferTest.setUp(); + } + + @BeforeEach + public void init() { + EXPECTED_PROTOCOL_STATUS.init(); + } + + + @Test + @Order(1) + public void testFailAndUnFailBuyBTCTrade(final TestInfo testInfo) { + TakeBuyBTCOfferTest test = new TakeBuyBTCOfferTest(); + test.testTakeAlicesBuyOffer(testInfo); + + var tradeId = test.getTradeId(); + aliceClient.failTrade(tradeId); + + Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getTrade(tradeId)); + String expectedExceptionMessage = format("INVALID_ARGUMENT: trade with id '%s' not found", tradeId); + assertEquals(expectedExceptionMessage, exception.getMessage()); + + try { + aliceClient.unFailTrade(tradeId); + aliceClient.getTrade(tradeId); //Throws ex if trade is still failed. + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(2) + public void testFailAndUnFailSellBTCTrade(final TestInfo testInfo) { + TakeSellBTCOfferTest test = new TakeSellBTCOfferTest(); + test.testTakeAlicesSellOffer(testInfo); + + var tradeId = test.getTradeId(); + aliceClient.failTrade(tradeId); + + Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getTrade(tradeId)); + String expectedExceptionMessage = format("INVALID_ARGUMENT: trade with id '%s' not found", tradeId); + assertEquals(expectedExceptionMessage, exception.getMessage()); + + try { + aliceClient.unFailTrade(tradeId); + aliceClient.getTrade(tradeId); //Throws ex if trade is still failed. + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(3) + public void testFailAndUnFailBuyXmrTrade(final TestInfo testInfo) { + TakeBuyXMROfferTest test = new TakeBuyXMROfferTest(); + test.createXmrPaymentAccounts(); + test.testTakeAlicesSellBTCForXMROffer(testInfo); + + var tradeId = test.getTradeId(); + aliceClient.failTrade(tradeId); + + Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getTrade(tradeId)); + String expectedExceptionMessage = format("INVALID_ARGUMENT: trade with id '%s' not found", tradeId); + assertEquals(expectedExceptionMessage, exception.getMessage()); + + try { + aliceClient.unFailTrade(tradeId); + aliceClient.getTrade(tradeId); //Throws ex if trade is still failed. + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(4) + public void testFailAndUnFailTakeSellXMRTrade(final TestInfo testInfo) { + TakeSellXMROfferTest test = new TakeSellXMROfferTest(); + test.createXmrPaymentAccounts(); + test.testTakeAlicesBuyBTCForXMROffer(testInfo); + + var tradeId = test.getTradeId(); + aliceClient.failTrade(tradeId); + + Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getTrade(tradeId)); + String expectedExceptionMessage = format("INVALID_ARGUMENT: trade with id '%s' not found", tradeId); + assertEquals(expectedExceptionMessage, exception.getMessage()); + + try { + aliceClient.unFailTrade(tradeId); + aliceClient.getTrade(tradeId); //Throws ex if trade is still failed. + } catch (Exception ex) { + fail(ex); + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java index 69d5ba6dd80..6e91705b4d8 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java @@ -30,6 +30,7 @@ import bisq.apitest.method.trade.AbstractTradeTest; import bisq.apitest.method.trade.BsqSwapTradeTest; +import bisq.apitest.method.trade.FailUnfailTradeTest; import bisq.apitest.method.trade.TakeBuyBSQOfferTest; import bisq.apitest.method.trade.TakeBuyBTCOfferTest; import bisq.apitest.method.trade.TakeBuyBTCOfferWithNationalBankAcctTest; @@ -130,4 +131,14 @@ public void testBsqSwapTradeTest(final TestInfo testInfo) { test.testBobTakesBsqSwapOffer(); test.testGetBalancesAfterTrade(); } + + @Test + @Order(9) + public void testFailUnfailTrade(final TestInfo testInfo) { + FailUnfailTradeTest test = new FailUnfailTradeTest(); + test.testFailAndUnFailBuyBTCTrade(testInfo); + test.testFailAndUnFailSellBTCTrade(testInfo); + test.testFailAndUnFailBuyXmrTrade(testInfo); + test.testFailAndUnFailTakeSellXMRTrade(testInfo); + } } diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index b0bd24b84c8..e42619df6b8 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -559,6 +559,28 @@ public static void run(String[] args) { paymentMethods.forEach(p -> out.println(p.getId())); return; } + case failtrade: { + var opts = new GetTradeOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var tradeId = opts.getTradeId(); + client.failTrade(tradeId); + out.printf("open trade %s changed to failed trade%n", tradeId); + return; + } + case unfailtrade: { + var opts = new GetTradeOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var tradeId = opts.getTradeId(); + client.unFailTrade(tradeId); + out.printf("failed trade %s changed to open trade%n", tradeId); + return; + } case getpaymentacctform: { var opts = new GetPaymentAcctFormOptionParser(args).parse(); if (opts.isForHelp()) { @@ -870,6 +892,10 @@ private static void printHelp(OptionParser parser, @SuppressWarnings("SameParame "Withdraw received trade funds to external wallet address"); stream.format(rowFormat, "", "[--memo=<\"memo\">]", ""); stream.println(); + stream.format(rowFormat, failtrade.name(), "--trade-id=", "Change open trade to failed trade"); + stream.println(); + stream.format(rowFormat, unfailtrade.name(), "--trade-id=", "Change failed trade to open trade"); + stream.println(); stream.format(rowFormat, getpaymentmethods.name(), "", "Get list of supported payment account method ids"); stream.println(); stream.format(rowFormat, getpaymentacctform.name(), "--payment-method-id=", "Get a new payment account form"); diff --git a/cli/src/main/java/bisq/cli/GrpcClient.java b/cli/src/main/java/bisq/cli/GrpcClient.java index 57312abc034..1d93187353d 100644 --- a/cli/src/main/java/bisq/cli/GrpcClient.java +++ b/cli/src/main/java/bisq/cli/GrpcClient.java @@ -380,6 +380,14 @@ public void withdrawFunds(String tradeId, String address, String memo) { tradesServiceRequest.withdrawFunds(tradeId, address, memo); } + public void failTrade(String tradeId) { + tradesServiceRequest.failTrade(tradeId); + } + + public void unFailTrade(String tradeId) { + tradesServiceRequest.unFailTrade(tradeId); + } + public List getPaymentMethods() { return paymentAccountsServiceRequest.getPaymentMethods(); } diff --git a/cli/src/main/java/bisq/cli/Method.java b/cli/src/main/java/bisq/cli/Method.java index 3e097238a86..55207203846 100644 --- a/cli/src/main/java/bisq/cli/Method.java +++ b/cli/src/main/java/bisq/cli/Method.java @@ -42,6 +42,8 @@ public enum Method { getpaymentaccts, getpaymentmethods, gettrade, + failtrade, + unfailtrade, gettransaction, gettxfeerate, getunusedbsqaddress, diff --git a/cli/src/main/java/bisq/cli/request/TradesServiceRequest.java b/cli/src/main/java/bisq/cli/request/TradesServiceRequest.java index 080be70e026..69b535a77e0 100644 --- a/cli/src/main/java/bisq/cli/request/TradesServiceRequest.java +++ b/cli/src/main/java/bisq/cli/request/TradesServiceRequest.java @@ -20,10 +20,12 @@ import bisq.proto.grpc.CloseTradeRequest; import bisq.proto.grpc.ConfirmPaymentReceivedRequest; import bisq.proto.grpc.ConfirmPaymentStartedRequest; +import bisq.proto.grpc.FailTradeRequest; import bisq.proto.grpc.GetTradeRequest; 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; @@ -103,4 +105,20 @@ public void withdrawFunds(String tradeId, String address, String memo) { //noinspection ResultOfMethodCallIgnored grpcStubs.tradesService.withdrawFunds(request); } + + public void failTrade(String tradeId) { + var request = FailTradeRequest.newBuilder() + .setTradeId(tradeId) + .build(); + //noinspection ResultOfMethodCallIgnored + grpcStubs.tradesService.failTrade(request); + } + + public void unFailTrade(String tradeId) { + var request = UnFailTradeRequest.newBuilder() + .setTradeId(tradeId) + .build(); + //noinspection ResultOfMethodCallIgnored + grpcStubs.tradesService.unFailTrade(request); + } } diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 2794db47bbb..57c9a07a503 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -335,6 +335,14 @@ public String getBsqSwapTradeRole(BsqSwapTrade bsqSwapTrade) { return coreTradesService.getBsqSwapTradeRole(bsqSwapTrade); } + public void failTrade(String tradeId) { + coreTradesService.failTrade(tradeId); + } + + public void unFailTrade(String tradeId) { + coreTradesService.unFailTrade(tradeId); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Wallets /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index f28e2f36cc2..d28ce99c736 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -25,6 +25,7 @@ import bisq.core.offer.bsq_swap.BsqSwapTakeOfferModel; 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.model.Tradable; @@ -63,6 +64,7 @@ class CoreTradesService { private final BtcWalletService btcWalletService; private final OfferUtil offerUtil; private final ClosedTradableManager closedTradableManager; + private final FailedTradesManager failedTradesManager; private final TakeOfferModel takeOfferModel; private final BsqSwapTakeOfferModel bsqSwapTakeOfferModel; private final TradeManager tradeManager; @@ -75,6 +77,7 @@ public CoreTradesService(CoreContext coreContext, BtcWalletService btcWalletService, OfferUtil offerUtil, ClosedTradableManager closedTradableManager, + FailedTradesManager failedTradesManager, TakeOfferModel takeOfferModel, BsqSwapTakeOfferModel bsqSwapTakeOfferModel, TradeManager tradeManager, @@ -85,6 +88,7 @@ public CoreTradesService(CoreContext coreContext, this.btcWalletService = btcWalletService; this.offerUtil = offerUtil; this.closedTradableManager = closedTradableManager; + this.failedTradesManager = failedTradesManager; this.takeOfferModel = takeOfferModel; this.bsqSwapTakeOfferModel = bsqSwapTakeOfferModel; this.tradeManager = tradeManager; @@ -269,6 +273,37 @@ Trade getTrade(String tradeId) { )); } + void failTrade(String tradeId) { + // TODO Recommend that API users should use 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(); + + var trade = getTrade(tradeId); + tradeManager.onMoveInvalidTradeToFailedTrades(trade); + log.info("Trade {} changed to failed trade.", tradeId); + } + + void unFailTrade(String tradeId) { + coreWalletsService.verifyWalletsAreAvailable(); + coreWalletsService.verifyEncryptedWalletIsUnlocked(); + + failedTradesManager.getTradeById(tradeId).ifPresentOrElse(failedTrade -> { + verifyCanUnfailTrade(failedTrade); + failedTradesManager.removeTrade(failedTrade); + tradeManager.addFailedTradeToPendingTrades(failedTrade); + log.info("Failed trade {} changed to open trade.", tradeId); + }, () -> { + throw new IllegalArgumentException(format("failed trade '%s' not found", tradeId)); + }); + } + private Optional getOpenTrade(String tradeId) { return tradeManager.getTradeById(tradeId); } @@ -318,4 +353,30 @@ private void verifyFundsNotWithdrawn(AddressEntry fromAddressEntry) { throw new IllegalStateException(format("funds already withdrawn from address '%s'", fromAddressEntry.getAddressString())); } + + // Throws a RuntimeException if failed trade cannot be changed to OPEN for any reason. + private void verifyCanUnfailTrade(Trade failedTrade) { + if (tradeUtil.getTradeAddresses(failedTrade) == null) + throw new IllegalStateException( + format("cannot change failed trade to open because no trade addresses found for '%s'", + failedTrade.getId())); + + if (!failedTradesManager.hasDepositTx(failedTrade)) + throw new IllegalStateException( + format("cannot change failed trade to open, no deposit tx found for '%s'", + failedTrade.getId())); + + if (!failedTradesManager.hasDelayedPayoutTxBytes(failedTrade)) + throw new IllegalStateException( + format("cannot change failed trade to open, no delayed payout tx found for '%s'", + failedTrade.getId())); + + failedTradesManager.getBlockingTradeIds(failedTrade).ifPresent(tradeIds -> { + throw new IllegalStateException( + format("cannot change failed trade '%s' to open at this time," + + "%ntry again after completing trade(s):%n\t%s", + failedTrade.getId(), + String.join(", ", tradeIds))); + }); + } } 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 412bacb03df..2ab852f9728 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 @@ -24,14 +24,20 @@ import bisq.core.trade.model.TradableList; import bisq.core.trade.model.bisq_v1.Trade; +import bisq.common.config.Config; import bisq.common.crypto.KeyRing; import bisq.common.persistence.PersistenceManager; import bisq.common.proto.persistable.PersistedDataHost; import com.google.inject.Inject; +import javax.inject.Named; + import javafx.collections.ObservableList; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; @@ -41,6 +47,8 @@ import lombok.Setter; +import static bisq.core.btc.model.AddressEntry.Context.AVAILABLE; + public class FailedTradesManager implements PersistedDataHost { private static final Logger log = LoggerFactory.getLogger(FailedTradesManager.class); private final TradableList failedTrades = new TradableList<>(); @@ -51,6 +59,8 @@ public class FailedTradesManager implements PersistedDataHost { private final PersistenceManager> persistenceManager; private final TradeUtil tradeUtil; private final DumpDelayedPayoutTx dumpDelayedPayoutTx; + private final boolean allowFaultyDelayedTxs; + @Setter private Predicate unFailTradeCallback; @@ -61,7 +71,8 @@ public FailedTradesManager(KeyRing keyRing, PersistenceManager> persistenceManager, TradeUtil tradeUtil, CleanupMailboxMessagesService cleanupMailboxMessagesService, - DumpDelayedPayoutTx dumpDelayedPayoutTx) { + DumpDelayedPayoutTx dumpDelayedPayoutTx, + @Named(Config.ALLOW_FAULTY_DELAYED_TXS) boolean allowFaultyDelayedTxs) { this.keyRing = keyRing; this.priceFeedService = priceFeedService; this.btcWalletService = btcWalletService; @@ -69,6 +80,7 @@ public FailedTradesManager(KeyRing keyRing, this.dumpDelayedPayoutTx = dumpDelayedPayoutTx; this.persistenceManager = persistenceManager; this.tradeUtil = tradeUtil; + this.allowFaultyDelayedTxs = allowFaultyDelayedTxs; this.persistenceManager.initialize(failedTrades, "FailedTrades", PersistenceManager.Source.PRIVATE); } @@ -136,17 +148,59 @@ public String checkUnFail(Trade trade) { if (addresses == null) { return "Addresses not found"; } - StringBuilder blockingTrades = new StringBuilder(); - for (var entry : btcWalletService.getAddressEntryListAsImmutableList()) { - if (entry.getContext() == AddressEntry.Context.AVAILABLE) - continue; - if (entry.getAddressString() != null && - (entry.getAddressString().equals(addresses.first) || - entry.getAddressString().equals(addresses.second))) { - blockingTrades.append(entry.getOfferId()).append(", "); + Optional> blockingTradeIds = getBlockingTradeIds(trade); + return blockingTradeIds.map(strings -> String.join(",", strings)).orElse(""); + } + + public Optional> getBlockingTradeIds(Trade trade) { + var tradeAddresses = tradeUtil.getTradeAddresses(trade); + if (tradeAddresses == null) { + return Optional.empty(); + } + + Predicate isBeingUsedForOtherTrade = (addressEntry) -> { + if (addressEntry.getContext() == AVAILABLE) { + return false; } + String address = addressEntry.getAddressString(); + return address != null + && (address.equals(tradeAddresses.first) || address.equals(tradeAddresses.second)); + }; + + List blockingTradeIds = new ArrayList<>(); + for (var addressEntry : btcWalletService.getAddressEntryListAsImmutableList()) { + if (isBeingUsedForOtherTrade.test(addressEntry)) { + var offerId = addressEntry.getOfferId(); + // TODO Be certain 'List blockingTrades' should NOT be populated + // with the trade parameter's tradeId. The 'var addressEntry' will + // always be found in the 'var tradeAddresses' tuple, so check + // offerId != trade.getId() to avoid the bug being fixed by the next if + // statement (if it was a bug). + if (!Objects.equals(offerId, trade.getId()) && !blockingTradeIds.contains(offerId)) + blockingTradeIds.add(offerId); + } + } + return blockingTradeIds.isEmpty() + ? Optional.empty() + : Optional.of(blockingTradeIds); + } + + public boolean hasDepositTx(Trade failedTrade) { + if (failedTrade.getDepositTx() == null) { + log.warn("Failed trade {} has no deposit tx.", failedTrade.getId()); + return false; + } else { + return true; + } + } + + public boolean hasDelayedPayoutTxBytes(Trade failedTrade) { + if (failedTrade.getDelayedPayoutTxBytes() != null) { + return true; + } else { + log.warn("Failed trade {} has no delayedPayoutTxBytes.", failedTrade.getId()); + return allowFaultyDelayedTxs; } - return blockingTrades.toString(); } private void requestPersistence() { diff --git a/core/src/main/resources/help/failtrade-help.txt b/core/src/main/resources/help/failtrade-help.txt new file mode 100644 index 00000000000..39d250d153e --- /dev/null +++ b/core/src/main/resources/help/failtrade-help.txt @@ -0,0 +1,28 @@ +failtrade + +NAME +---- +failtrade - change an open trade to a failed trade + +SYNOPSIS +-------- +failtrade + --trade-id= + +DESCRIPTION +----------- +If there are problems with an existing trade, and it cannot be completed, it can be moved +from the open trades list to the failed trades list. + +You cannot open mediation or arbitration for a failed trade, but you can change a failed +trade back to an open trade with the 'unfailtrade' command. + +OPTIONS +------- +--trade-id + The ID of the trade (the full offer-id). + +EXAMPLES +-------- +Change the status of an active, open trade to failed: +$ ./bisq-cli --password=xyz --port=9998 failtrade --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/core/src/main/resources/help/unfailtrade-help.txt b/core/src/main/resources/help/unfailtrade-help.txt new file mode 100644 index 00000000000..f82144dcf38 --- /dev/null +++ b/core/src/main/resources/help/unfailtrade-help.txt @@ -0,0 +1,32 @@ +unfailtrade + +NAME +---- +unfailtrade - change a failed trade to an open trade + +SYNOPSIS +-------- +unfailtrade + --trade-id= + +DESCRIPTION +----------- +This is a possible way to unlock funds stuck in a failed trade. + +The operation could fail for any of the following reasons: + + The trade's deposit transaction is missing (null). + The trade's delayed payout transaction is missing (null). + The trade is using wallet addresses also being used one or more other trades. + +Before proceeding, make sure you have a backup of your data directory. + +OPTIONS +------- +--trade-id + The ID of the trade (the full offer-id). + +EXAMPLES +-------- +Change the status of failed trade back to an open trade: +$ ./bisq-cli --password=xyz --port=9998 unfailtrade --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java index 38d03382552..dbcb4c6c51d 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -29,10 +29,14 @@ import bisq.proto.grpc.ConfirmPaymentReceivedRequest; import bisq.proto.grpc.ConfirmPaymentStartedReply; import bisq.proto.grpc.ConfirmPaymentStartedRequest; +import bisq.proto.grpc.FailTradeReply; +import bisq.proto.grpc.FailTradeRequest; import bisq.proto.grpc.GetTradeReply; import bisq.proto.grpc.GetTradeRequest; import bisq.proto.grpc.TakeOfferReply; import bisq.proto.grpc.TakeOfferRequest; +import bisq.proto.grpc.UnFailTradeReply; +import bisq.proto.grpc.UnFailTradeRequest; import bisq.proto.grpc.WithdrawFundsReply; import bisq.proto.grpc.WithdrawFundsRequest; @@ -177,6 +181,32 @@ public void withdrawFunds(WithdrawFundsRequest req, } } + @Override + public void failTrade(FailTradeRequest req, + StreamObserver responseObserver) { + try { + coreApi.failTrade(req.getTradeId()); + var reply = FailTradeReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void unFailTrade(UnFailTradeRequest req, + StreamObserver responseObserver) { + try { + coreApi.unFailTrade(req.getTradeId()); + var reply = UnFailTradeReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 1af7e69ab01..bb4504c74c9 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -396,6 +396,10 @@ service Trades { } rpc CloseTrade (CloseTradeRequest) returns (CloseTradeReply) { } + rpc FailTrade (FailTradeRequest) returns (FailTradeReply) { + } + rpc UnFailTrade (UnFailTradeRequest) returns (UnFailTradeReply) { + } rpc WithdrawFunds (WithdrawFundsRequest) returns (WithdrawFundsReply) { } } @@ -440,6 +444,20 @@ message CloseTradeRequest { message CloseTradeReply { } +message FailTradeRequest { + string tradeId = 1; +} + +message FailTradeReply { +} + +message UnFailTradeRequest { + string tradeId = 1; +} + +message UnFailTradeReply { +} + message WithdrawFundsRequest { string tradeId = 1; string address = 2;