Merge pull request #4917 from chimp1984/handle-invalid-maker-fee-tx

Detect and handle invalid maker fee tx
This commit is contained in:
Christoph Atteneder 2020-12-10 11:57:32 +01:00 committed by GitHub
commit 9498fd9b7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 131 additions and 3 deletions

View file

@ -19,6 +19,7 @@ package bisq.core.offer;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.filter.FilterManager;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
@ -44,6 +45,9 @@ import bisq.common.util.MathUtils;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.utils.Fiat;
import javax.inject.Inject;
@ -383,4 +387,53 @@ public class OfferUtil {
return Optional.empty();
}
}
public static Optional<String> getInvalidMakerFeeTxErrorMessage(Offer offer, BtcWalletService btcWalletService) {
Transaction makerFeeTx = btcWalletService.getTransaction(offer.getOfferFeePaymentTxId());
if (makerFeeTx == null) {
return Optional.empty();
}
String errorMsg = null;
String header = "The offer with offer ID '" + offer.getShortId() +
"' has an invalid maker fee transaction.\n\n";
String spendingTransaction = null;
String extraString = "\nYou have to remove that offer to avoid failed trades.\n" +
"If this happened because of a bug please contact the Bisq developers " +
"and you can request reimbursement for the lost maker fee.";
if (makerFeeTx.getOutputs().size() > 1) {
// Our output to fund the deposit tx is at index 1
TransactionOutput output = makerFeeTx.getOutput(1);
TransactionInput spentByTransactionInput = output.getSpentBy();
if (spentByTransactionInput != null) {
spendingTransaction = spentByTransactionInput.getConnectedTransaction() != null ?
spentByTransactionInput.getConnectedTransaction().toString() :
"null";
// We this is an exceptional case we do not translate that error msg.
errorMsg = "The output of the maker fee tx is already spent.\n" +
extraString +
"\n\nTransaction input which spent the reserved funds for that offer: '" +
spentByTransactionInput.getConnectedTransaction().getTxId().toString() + ":" +
(spentByTransactionInput.getConnectedOutput() != null ?
spentByTransactionInput.getConnectedOutput().getIndex() + "'" :
"null'");
log.error("spentByTransactionInput {}", spentByTransactionInput);
}
} else {
errorMsg = "The maker fee tx is invalid as it does not has at least 2 outputs." + extraString +
"\nMakerFeeTx=" + makerFeeTx.toString();
}
if (errorMsg == null) {
return Optional.empty();
}
errorMsg = header + errorMsg;
log.error(errorMsg);
if (spendingTransaction != null) {
log.error("Spending transaction: {}", spendingTransaction);
}
return Optional.of(errorMsg);
}
}

View file

@ -63,11 +63,13 @@ import bisq.common.handlers.ResultHandler;
import bisq.common.persistence.PersistenceManager;
import bisq.common.proto.network.NetworkEnvelope;
import bisq.common.proto.persistable.PersistedDataHost;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Coin;
import javax.inject.Inject;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.ArrayList;
@ -82,6 +84,8 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
@ -118,6 +122,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private final TradableList<OpenOffer> openOffers = new TradableList<>();
private boolean stopped;
private Timer periodicRepublishOffersTimer, periodicRefreshOffersTimer, retryRepublishOffersTimer;
@Getter
private final ObservableList<Tuple2<OpenOffer, String>> invalidOffers = FXCollections.observableArrayList();
///////////////////////////////////////////////////////////////////////////////////////////
@ -190,6 +196,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
cleanUpAddressEntries();
openOffers.stream()
.forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService)
.ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg))));
}
private void cleanUpAddressEntries() {

View file

@ -48,6 +48,8 @@ import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.locale.CryptoCurrency;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.AliPayAccount;
import bisq.core.payment.CryptoCurrencyAccount;
import bisq.core.payment.RevolutAccount;
@ -70,6 +72,7 @@ import bisq.common.app.DevEnv;
import bisq.common.app.Version;
import bisq.common.config.Config;
import bisq.common.file.CorruptedStorageFileHandler;
import bisq.common.util.Tuple2;
import com.google.inject.Inject;
@ -86,6 +89,7 @@ import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.ArrayList;
@ -115,6 +119,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
private final SettingsPresentation settingsPresentation;
private final P2PService p2PService;
private final TradeManager tradeManager;
private final OpenOfferManager openOfferManager;
@Getter
private final Preferences preferences;
private final PrivateNotificationManager privateNotificationManager;
@ -160,6 +165,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
SettingsPresentation settingsPresentation,
P2PService p2PService,
TradeManager tradeManager,
OpenOfferManager openOfferManager,
Preferences preferences,
PrivateNotificationManager privateNotificationManager,
WalletPasswordWindow walletPasswordWindow,
@ -184,6 +190,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
this.settingsPresentation = settingsPresentation;
this.p2PService = p2PService;
this.tradeManager = tradeManager;
this.openOfferManager = openOfferManager;
this.preferences = preferences;
this.privateNotificationManager = privateNotificationManager;
this.walletPasswordWindow = walletPasswordWindow;
@ -432,6 +439,17 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
this.footerVersionInfo.setValue("v" + Version.VERSION);
}
});
if (p2PService.isBootstrapped()) {
setupInvalidOpenOffersHandler();
} else {
p2PService.addP2PServiceListener(new BootstrapListener() {
@Override
public void onUpdatedDataReceived() {
setupInvalidOpenOffersHandler();
}
});
}
}
private void showRevolutAccountUpdateWindow(List<RevolutAccount> revolutAccountList) {
@ -573,6 +591,34 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
}
}
private void setupInvalidOpenOffersHandler() {
openOfferManager.getInvalidOffers().addListener((ListChangeListener<Tuple2<OpenOffer, String>>) c -> {
c.next();
if (c.wasAdded()) {
handleInvalidOpenOffers(c.getAddedSubList());
}
});
handleInvalidOpenOffers(openOfferManager.getInvalidOffers());
}
private void handleInvalidOpenOffers(List<? extends Tuple2<OpenOffer, String>> list) {
list.forEach(tuple2 -> {
String errorMsg = tuple2.second;
OpenOffer openOffer = tuple2.first;
new Popup().warning(errorMsg)
.width(1000)
.actionButtonText(Res.get("shared.removeOffer"))
.onAction(() -> {
openOfferManager.removeOpenOffer(openOffer, () -> {
log.info("Invalid open offer with ID {} was successfully removed.", openOffer.getId());
}, log::error);
})
.hideCloseButton()
.show();
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// MainView delegate getters

View file

@ -20,17 +20,21 @@ package bisq.desktop.main.overlays.windows;
import bisq.desktop.Navigation;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.BusyAnimation;
import bisq.desktop.components.TitledGroupBg;
import bisq.desktop.components.TxIdTextField;
import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil;
import bisq.desktop.util.Layout;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.locale.BankUtil;
import bisq.core.locale.CountryUtil;
import bisq.core.locale.Res;
import bisq.core.monetary.Price;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferUtil;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.user.User;
@ -74,6 +78,7 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
private final User user;
private final KeyRing keyRing;
private final Navigation navigation;
private final BtcWalletService btcWalletService;
private Offer offer;
private Coin tradeAmount;
private Price tradePrice;
@ -90,11 +95,13 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
public OfferDetailsWindow(@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
User user,
KeyRing keyRing,
Navigation navigation) {
Navigation navigation,
BtcWalletService btcWalletService) {
this.formatter = formatter;
this.user = user;
this.keyRing = keyRing;
this.navigation = navigation;
this.btcWalletService = btcWalletService;
type = Type.Confirmation;
}
@ -313,13 +320,13 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
textArea.setEditable(false);
}
rows = 3;
rows = 4;
if (countryCode != null)
rows++;
if (!isF2F)
rows++;
addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.GROUP_DISTANCE);
TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.GROUP_DISTANCE);
addConfirmationLabelTextFieldWithCopyIcon(gridPane, rowIndex, Res.get("shared.offerId"), offer.getId(),
Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE);
addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("offerDetailsWindow.makersOnion"),
@ -335,6 +342,18 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
formatter.formatCoinWithCode(offer.getSellerSecurityDeposit());
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), value);
TxIdTextField makerFeeTxIdTextField = addLabelTxIdTextField(gridPane, ++rowIndex,
Res.get("shared.makerFeeTxId"), offer.getOfferFeePaymentTxId()).second;
int finalRows = rows;
OfferUtil.getInvalidMakerFeeTxErrorMessage(offer, btcWalletService)
.ifPresent(errorMsg -> {
makerFeeTxIdTextField.getTextField().setId("address-text-field-error");
GridPane.setRowSpan(titledGroupBg, finalRows + 1);
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.errorMessage"),
errorMsg.replace("\n\n", "\n"));
});
if (countryCode != null && !isF2F)
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.countryBank"),
CountryUtil.getNameAndCode(countryCode));