mirror of
https://github.com/bisq-network/bisq.git
synced 2025-02-25 07:27:18 +01:00
Merge pull request #4917 from chimp1984/handle-invalid-maker-fee-tx
Detect and handle invalid maker fee tx
This commit is contained in:
commit
9498fd9b7d
4 changed files with 131 additions and 3 deletions
|
@ -19,6 +19,7 @@ package bisq.core.offer;
|
||||||
|
|
||||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||||
import bisq.core.btc.wallet.BsqWalletService;
|
import bisq.core.btc.wallet.BsqWalletService;
|
||||||
|
import bisq.core.btc.wallet.BtcWalletService;
|
||||||
import bisq.core.filter.FilterManager;
|
import bisq.core.filter.FilterManager;
|
||||||
import bisq.core.locale.CurrencyUtil;
|
import bisq.core.locale.CurrencyUtil;
|
||||||
import bisq.core.locale.Res;
|
import bisq.core.locale.Res;
|
||||||
|
@ -44,6 +45,9 @@ import bisq.common.util.MathUtils;
|
||||||
import bisq.common.util.Tuple2;
|
import bisq.common.util.Tuple2;
|
||||||
|
|
||||||
import org.bitcoinj.core.Coin;
|
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 org.bitcoinj.utils.Fiat;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
@ -383,4 +387,53 @@ public class OfferUtil {
|
||||||
return Optional.empty();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,11 +63,13 @@ import bisq.common.handlers.ResultHandler;
|
||||||
import bisq.common.persistence.PersistenceManager;
|
import bisq.common.persistence.PersistenceManager;
|
||||||
import bisq.common.proto.network.NetworkEnvelope;
|
import bisq.common.proto.network.NetworkEnvelope;
|
||||||
import bisq.common.proto.persistable.PersistedDataHost;
|
import bisq.common.proto.persistable.PersistedDataHost;
|
||||||
|
import bisq.common.util.Tuple2;
|
||||||
|
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -82,6 +84,8 @@ import java.util.stream.Collectors;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
@ -118,6 +122,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
private final TradableList<OpenOffer> openOffers = new TradableList<>();
|
private final TradableList<OpenOffer> openOffers = new TradableList<>();
|
||||||
private boolean stopped;
|
private boolean stopped;
|
||||||
private Timer periodicRepublishOffersTimer, periodicRefreshOffersTimer, retryRepublishOffersTimer;
|
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();
|
cleanUpAddressEntries();
|
||||||
|
|
||||||
|
openOffers.stream()
|
||||||
|
.forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService)
|
||||||
|
.ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg))));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cleanUpAddressEntries() {
|
private void cleanUpAddressEntries() {
|
||||||
|
|
|
@ -48,6 +48,8 @@ import bisq.core.btc.wallet.BtcWalletService;
|
||||||
import bisq.core.locale.CryptoCurrency;
|
import bisq.core.locale.CryptoCurrency;
|
||||||
import bisq.core.locale.CurrencyUtil;
|
import bisq.core.locale.CurrencyUtil;
|
||||||
import bisq.core.locale.Res;
|
import bisq.core.locale.Res;
|
||||||
|
import bisq.core.offer.OpenOffer;
|
||||||
|
import bisq.core.offer.OpenOfferManager;
|
||||||
import bisq.core.payment.AliPayAccount;
|
import bisq.core.payment.AliPayAccount;
|
||||||
import bisq.core.payment.CryptoCurrencyAccount;
|
import bisq.core.payment.CryptoCurrencyAccount;
|
||||||
import bisq.core.payment.RevolutAccount;
|
import bisq.core.payment.RevolutAccount;
|
||||||
|
@ -70,6 +72,7 @@ import bisq.common.app.DevEnv;
|
||||||
import bisq.common.app.Version;
|
import bisq.common.app.Version;
|
||||||
import bisq.common.config.Config;
|
import bisq.common.config.Config;
|
||||||
import bisq.common.file.CorruptedStorageFileHandler;
|
import bisq.common.file.CorruptedStorageFileHandler;
|
||||||
|
import bisq.common.util.Tuple2;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
@ -86,6 +89,7 @@ import javafx.beans.property.SimpleDoubleProperty;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.property.StringProperty;
|
import javafx.beans.property.StringProperty;
|
||||||
|
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -115,6 +119,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
|
||||||
private final SettingsPresentation settingsPresentation;
|
private final SettingsPresentation settingsPresentation;
|
||||||
private final P2PService p2PService;
|
private final P2PService p2PService;
|
||||||
private final TradeManager tradeManager;
|
private final TradeManager tradeManager;
|
||||||
|
private final OpenOfferManager openOfferManager;
|
||||||
@Getter
|
@Getter
|
||||||
private final Preferences preferences;
|
private final Preferences preferences;
|
||||||
private final PrivateNotificationManager privateNotificationManager;
|
private final PrivateNotificationManager privateNotificationManager;
|
||||||
|
@ -160,6 +165,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
|
||||||
SettingsPresentation settingsPresentation,
|
SettingsPresentation settingsPresentation,
|
||||||
P2PService p2PService,
|
P2PService p2PService,
|
||||||
TradeManager tradeManager,
|
TradeManager tradeManager,
|
||||||
|
OpenOfferManager openOfferManager,
|
||||||
Preferences preferences,
|
Preferences preferences,
|
||||||
PrivateNotificationManager privateNotificationManager,
|
PrivateNotificationManager privateNotificationManager,
|
||||||
WalletPasswordWindow walletPasswordWindow,
|
WalletPasswordWindow walletPasswordWindow,
|
||||||
|
@ -184,6 +190,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
|
||||||
this.settingsPresentation = settingsPresentation;
|
this.settingsPresentation = settingsPresentation;
|
||||||
this.p2PService = p2PService;
|
this.p2PService = p2PService;
|
||||||
this.tradeManager = tradeManager;
|
this.tradeManager = tradeManager;
|
||||||
|
this.openOfferManager = openOfferManager;
|
||||||
this.preferences = preferences;
|
this.preferences = preferences;
|
||||||
this.privateNotificationManager = privateNotificationManager;
|
this.privateNotificationManager = privateNotificationManager;
|
||||||
this.walletPasswordWindow = walletPasswordWindow;
|
this.walletPasswordWindow = walletPasswordWindow;
|
||||||
|
@ -432,6 +439,17 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
|
||||||
this.footerVersionInfo.setValue("v" + Version.VERSION);
|
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) {
|
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
|
// MainView delegate getters
|
||||||
|
|
|
@ -20,17 +20,21 @@ package bisq.desktop.main.overlays.windows;
|
||||||
import bisq.desktop.Navigation;
|
import bisq.desktop.Navigation;
|
||||||
import bisq.desktop.components.AutoTooltipButton;
|
import bisq.desktop.components.AutoTooltipButton;
|
||||||
import bisq.desktop.components.BusyAnimation;
|
import bisq.desktop.components.BusyAnimation;
|
||||||
|
import bisq.desktop.components.TitledGroupBg;
|
||||||
|
import bisq.desktop.components.TxIdTextField;
|
||||||
import bisq.desktop.main.overlays.Overlay;
|
import bisq.desktop.main.overlays.Overlay;
|
||||||
import bisq.desktop.util.DisplayUtils;
|
import bisq.desktop.util.DisplayUtils;
|
||||||
import bisq.desktop.util.GUIUtil;
|
import bisq.desktop.util.GUIUtil;
|
||||||
import bisq.desktop.util.Layout;
|
import bisq.desktop.util.Layout;
|
||||||
|
|
||||||
|
import bisq.core.btc.wallet.BtcWalletService;
|
||||||
import bisq.core.locale.BankUtil;
|
import bisq.core.locale.BankUtil;
|
||||||
import bisq.core.locale.CountryUtil;
|
import bisq.core.locale.CountryUtil;
|
||||||
import bisq.core.locale.Res;
|
import bisq.core.locale.Res;
|
||||||
import bisq.core.monetary.Price;
|
import bisq.core.monetary.Price;
|
||||||
import bisq.core.offer.Offer;
|
import bisq.core.offer.Offer;
|
||||||
import bisq.core.offer.OfferPayload;
|
import bisq.core.offer.OfferPayload;
|
||||||
|
import bisq.core.offer.OfferUtil;
|
||||||
import bisq.core.payment.PaymentAccount;
|
import bisq.core.payment.PaymentAccount;
|
||||||
import bisq.core.payment.payload.PaymentMethod;
|
import bisq.core.payment.payload.PaymentMethod;
|
||||||
import bisq.core.user.User;
|
import bisq.core.user.User;
|
||||||
|
@ -74,6 +78,7 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
|
||||||
private final User user;
|
private final User user;
|
||||||
private final KeyRing keyRing;
|
private final KeyRing keyRing;
|
||||||
private final Navigation navigation;
|
private final Navigation navigation;
|
||||||
|
private final BtcWalletService btcWalletService;
|
||||||
private Offer offer;
|
private Offer offer;
|
||||||
private Coin tradeAmount;
|
private Coin tradeAmount;
|
||||||
private Price tradePrice;
|
private Price tradePrice;
|
||||||
|
@ -90,11 +95,13 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
|
||||||
public OfferDetailsWindow(@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
|
public OfferDetailsWindow(@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
|
||||||
User user,
|
User user,
|
||||||
KeyRing keyRing,
|
KeyRing keyRing,
|
||||||
Navigation navigation) {
|
Navigation navigation,
|
||||||
|
BtcWalletService btcWalletService) {
|
||||||
this.formatter = formatter;
|
this.formatter = formatter;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.keyRing = keyRing;
|
this.keyRing = keyRing;
|
||||||
this.navigation = navigation;
|
this.navigation = navigation;
|
||||||
|
this.btcWalletService = btcWalletService;
|
||||||
type = Type.Confirmation;
|
type = Type.Confirmation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,13 +320,13 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
|
||||||
textArea.setEditable(false);
|
textArea.setEditable(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
rows = 3;
|
rows = 4;
|
||||||
if (countryCode != null)
|
if (countryCode != null)
|
||||||
rows++;
|
rows++;
|
||||||
if (!isF2F)
|
if (!isF2F)
|
||||||
rows++;
|
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(),
|
addConfirmationLabelTextFieldWithCopyIcon(gridPane, rowIndex, Res.get("shared.offerId"), offer.getId(),
|
||||||
Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE);
|
Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE);
|
||||||
addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("offerDetailsWindow.makersOnion"),
|
addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("offerDetailsWindow.makersOnion"),
|
||||||
|
@ -335,6 +342,18 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
|
||||||
formatter.formatCoinWithCode(offer.getSellerSecurityDeposit());
|
formatter.formatCoinWithCode(offer.getSellerSecurityDeposit());
|
||||||
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), value);
|
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)
|
if (countryCode != null && !isF2F)
|
||||||
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.countryBank"),
|
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.countryBank"),
|
||||||
CountryUtil.getNameAndCode(countryCode));
|
CountryUtil.getNameAndCode(countryCode));
|
||||||
|
|
Loading…
Add table
Reference in a new issue