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;