Reattach addresses when unfailing trade

This commit is contained in:
sqrrm 2020-04-10 16:25:05 +02:00
parent bd8e30c708
commit 817819dc51
No known key found for this signature in database
GPG Key ID: 45235F9EF87089EC
7 changed files with 165 additions and 11 deletions

View File

@ -598,6 +598,13 @@ public class BtcWalletService extends WalletService {
return entry;
}
public AddressEntry recoverAddressEntry(String offerId, String address, AddressEntry.Context context) {
var available = findAddressEntry(address, AddressEntry.Context.AVAILABLE);
if (!available.isPresent())
return null;
return addressEntryList.swapAvailableToAddressEntryWithOfferId(available.get(), context, offerId);
}
private AddressEntry getOrCreateAddressEntry(AddressEntry.Context context, Optional<AddressEntry> addressEntry) {
if (addressEntry.isPresent()) {
return addressEntry.get();

View File

@ -64,6 +64,8 @@ import bisq.common.handlers.ResultHandler;
import bisq.common.proto.network.NetworkEnvelope;
import bisq.common.proto.persistable.PersistedDataHost;
import bisq.common.storage.Storage;
import bisq.common.util.Tuple2;
import bisq.common.util.Utilities;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
@ -89,6 +91,7 @@ import org.spongycastle.crypto.params.KeyParameter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@ -604,12 +607,35 @@ public class TradeManager implements PersistedDataHost {
}
// If trade still has funds locked up it might come back from failed trades
private void unfailTrade(Trade trade) {
// Aborts unfailing if the address entries needed are not available
private boolean unfailTrade(Trade trade) {
if (!recoverAddresses(trade)) {
log.warn("Failed to recover address during unfail trade");
return false;
}
if (!tradableList.contains(trade)) {
tradableList.add(trade);
}
return true;
}
// The trade is added to pending trades if the associated address entries are AVAILABLE and
// the relevant entries are changed, otherwise it's not added and no address entries are changed
private boolean recoverAddresses(Trade trade) {
// Find addresses associated with this trade.
var entries = TradeUtils.getAvailableAddresses(trade, btcWalletService, keyRing);
if (entries == null)
return false;
btcWalletService.recoverAddressEntry(trade.getId(), entries.first,
AddressEntry.Context.MULTI_SIG);
btcWalletService.recoverAddressEntry(trade.getId(), entries.second,
AddressEntry.Context.TRADE_PAYOUT);
return true;
}
// If trade is in preparation (if taker role: before taker fee is paid; both roles: before deposit published)
// we just remove the trade from our list. We don't store those trades.
public void removePreparedTrade(Trade trade) {

View File

@ -0,0 +1,79 @@
/*
* 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.trade;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.common.crypto.KeyRing;
import bisq.common.util.Tuple2;
import bisq.common.util.Utilities;
import java.util.Objects;
public class TradeUtils {
// Returns <MULTI_SIG, TRADE_PAYOUT> if both are AVAILABLE, otherwise null
static Tuple2<String, String> getAvailableAddresses(Trade trade, BtcWalletService btcWalletService,
KeyRing keyRing) {
var addresses = getTradeAddresses(trade, btcWalletService, keyRing);
if (addresses == null)
return null;
if (btcWalletService.getAvailableAddressEntries().stream()
.noneMatch(e -> Objects.equals(e.getAddressString(), addresses.first)))
return null;
if (btcWalletService.getAvailableAddressEntries().stream()
.noneMatch(e -> Objects.equals(e.getAddressString(), addresses.second)))
return null;
return new Tuple2<>(addresses.first, addresses.second);
}
// Returns <MULTI_SIG, TRADE_PAYOUT> addresses as strings if they're known by the wallet
public static Tuple2<String, String> getTradeAddresses(Trade trade, BtcWalletService btcWalletService,
KeyRing keyRing) {
var contract = trade.getContract();
if (contract == null)
return null;
// Get multisig address
var isMyRoleBuyer = contract.isMyRoleBuyer(keyRing.getPubKeyRing());
var multiSigPubKey = isMyRoleBuyer ? contract.getBuyerMultiSigPubKey() : contract.getSellerMultiSigPubKey();
if (multiSigPubKey == null)
return null;
var multiSigPubKeyString = Utilities.bytesAsHexString(multiSigPubKey);
var multiSigAddress = btcWalletService.getAddressEntryListAsImmutableList().stream()
.filter(e -> e.getKeyPair().getPublicKeyAsHex().equals(multiSigPubKeyString))
.findAny()
.orElse(null);
if (multiSigAddress == null)
return null;
// Get payout address
var payoutAddress = isMyRoleBuyer ?
contract.getBuyerPayoutAddressString() : contract.getSellerPayoutAddressString();
var payoutAddressEntry = btcWalletService.getAddressEntryListAsImmutableList().stream()
.filter(e -> Objects.equals(e.getAddressString(), payoutAddress))
.findAny()
.orElse(null);
if (payoutAddressEntry == null)
return null;
return new Tuple2<>(multiSigAddress.getAddressString(), payoutAddress);
}
}

View File

@ -17,12 +17,14 @@
package bisq.core.trade.failed;
import bisq.core.btc.model.AddressEntry;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.offer.Offer;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.DumpDelayedPayoutTx;
import bisq.core.trade.TradableList;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeUtils;
import bisq.common.crypto.KeyRing;
import bisq.common.proto.persistable.PersistedDataHost;
@ -33,7 +35,7 @@ import com.google.inject.Inject;
import javafx.collections.ObservableList;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import org.slf4j.Logger;
@ -50,7 +52,7 @@ public class FailedTradesManager implements PersistedDataHost {
private final Storage<TradableList<Trade>> tradableListStorage;
private final DumpDelayedPayoutTx dumpDelayedPayoutTx;
@Setter
private Consumer<Trade> unfailTradeCallback;
private Function<Trade, Boolean> unfailTradeCallback;
@Inject
public FailedTradesManager(KeyRing keyRing,
@ -103,8 +105,29 @@ public class FailedTradesManager implements PersistedDataHost {
}
public void unfailTrade(Trade trade) {
if (unfailTradeCallback == null) return;
unfailTradeCallback.accept(trade);
failedTrades.remove(trade);
if (unfailTradeCallback == null)
return;
if (unfailTradeCallback.apply(trade)) {
failedTrades.remove(trade);
}
}
public String checkUnfail(Trade trade) {
var addresses = TradeUtils.getTradeAddresses(trade, btcWalletService, keyRing);
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(", ");
}
}
return blockingTrades.toString();
}
}

View File

@ -859,8 +859,11 @@ portfolio.closed.ticketClosed=Arbitrated
portfolio.closed.mediationTicketClosed=Mediated
portfolio.closed.canceled=Canceled
portfolio.failed.Failed=Failed
portfolio.failed.unfail=Do you want to move this trade back to pending trades? \
Only do this if you need to open a support ticket.
portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\n\
Do you want to move this trade back to open trades?\n\
This is a way to unlock funds stuck in a failed trade.
portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \n\
Try again after completion of trade(s) {0}
####################################################################

View File

@ -77,4 +77,8 @@ class FailedTradesDataModel extends ActivatableDataModel {
public void unfailTrade(Trade trade) {
failedTradesManager.unfailTrade(trade);
}
public String checkUnfail(Trade trade) {
return failedTradesManager.checkUnfail(trade);
}
}

View File

@ -108,9 +108,16 @@ public class FailedTradesView extends ActivatableViewAndModel<VBox, FailedTrades
keyEventEventHandler = keyEvent -> {
if (Utilities.isAltOrCtrlPressed(KeyCode.Y, keyEvent)) {
new Popup().warning(Res.get("portfolio.failed.unfail"))
.onAction(this::onUnfail)
.show();
var checkUnfailString = checkUnfail();
if (!checkUnfailString.isEmpty()) {
new Popup().warning(Res.get("portfolio.failed.cantUnfail", checkUnfailString))
.onAction(this::onUnfail)
.show();
} else {
new Popup().warning(Res.get("portfolio.failed.unfail"))
.onAction(this::onUnfail)
.show();
}
}
};
@ -119,6 +126,11 @@ public class FailedTradesView extends ActivatableViewAndModel<VBox, FailedTrades
private void onUnfail() {
Trade trade = sortedList.get(tableView.getSelectionModel().getFocusedIndex()).getTrade();
model.dataModel.unfailTrade(trade);
}
private String checkUnfail() {
Trade trade = sortedList.get(tableView.getSelectionModel().getFocusedIndex()).getTrade();
return model.dataModel.checkUnfail(trade);
}