From 9fcd65f3208798e50f2454217254fecc09d32958 Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Wed, 3 Mar 2021 22:19:23 -0600 Subject: [PATCH] Validate maker/taker fees using mempool lookup apply @chimp1984 patch.txt code review suggestions taker tx check moved to trade step 2, after confirmation solve issue with calculating expected fees for unconfirmed tx resolve conflict with PR5207 (Disputes UI) check new offers after 1 block; check Json string not null; warn -> info remove unused parameter remove debugging log.warn message --- .../main/java/bisq/common/config/Config.java | 10 + .../bisq/core/app/DomainInitialisation.java | 7 +- .../main/java/bisq/core/filter/Filter.java | 12 + .../main/java/bisq/core/offer/OpenOffer.java | 3 + .../bisq/core/offer/TriggerPriceService.java | 18 + .../bisq/core/provider/MempoolHttpClient.java | 45 ++ .../core/provider/mempool/MempoolRequest.java | 86 ++++ .../core/provider/mempool/MempoolService.java | 275 ++++++++++++ .../core/provider/mempool/TxValidator.java | 395 ++++++++++++++++++ .../bisq/core/trade/TradeDataValidation.java | 21 + .../resources/i18n/displayStrings.properties | 15 +- .../provider/mempool/TxValidatorTest.java | 278 ++++++++++++ .../core/user/UserPayloadModelVOTest.java | 1 + .../core/util/FeeReceiverSelectorTest.java | 1 + .../provider/mempool/badOfferTestData.json | 13 + .../core/provider/mempool/offerTestData.json | 19 + .../bisq/core/provider/mempool/txInfo.json | 27 ++ .../offer/takeoffer/TakeOfferDataModel.java | 21 + .../main/offer/takeoffer/TakeOfferView.java | 23 +- .../offer/takeoffer/TakeOfferViewModel.java | 11 + .../windows/DisputeSummaryWindow.java | 55 ++- .../main/overlays/windows/FilterWindow.java | 4 + .../pendingtrades/PendingTradesView.java | 12 + .../pendingtrades/PendingTradesViewModel.java | 35 ++ .../steps/buyer/BuyerStep1View.java | 14 +- .../steps/buyer/BuyerStep2View.java | 3 +- .../steps/seller/SellerStep1View.java | 23 + .../steps/seller/SellerStep2View.java | 5 + proto/src/main/proto/pb.proto | 1 + 29 files changed, 1420 insertions(+), 13 deletions(-) create mode 100644 core/src/main/java/bisq/core/provider/MempoolHttpClient.java create mode 100644 core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java create mode 100644 core/src/main/java/bisq/core/provider/mempool/MempoolService.java create mode 100644 core/src/main/java/bisq/core/provider/mempool/TxValidator.java create mode 100644 core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java create mode 100644 core/src/test/resources/bisq/core/provider/mempool/badOfferTestData.json create mode 100644 core/src/test/resources/bisq/core/provider/mempool/offerTestData.json create mode 100644 core/src/test/resources/bisq/core/provider/mempool/txInfo.json diff --git a/common/src/main/java/bisq/common/config/Config.java b/common/src/main/java/bisq/common/config/Config.java index f824d8ff79f..bc21c70a131 100644 --- a/common/src/main/java/bisq/common/config/Config.java +++ b/common/src/main/java/bisq/common/config/Config.java @@ -124,6 +124,7 @@ public class Config { public static final String BTC_TX_FEE = "btcTxFee"; public static final String BTC_MIN_TX_FEE = "btcMinTxFee"; public static final String BTC_FEES_TS = "bitcoinFeesTs"; + public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation"; // Default values for certain options public static final int UNSPECIFIED_PORT = -1; @@ -212,6 +213,7 @@ public class Config { public final int apiPort; public final boolean preventPeriodicShutdownAtSeedNode; public final boolean republishMailboxEntries; + public final boolean bypassMempoolValidation; // Properties derived from options but not exposed as options themselves public final File torDir; @@ -660,6 +662,13 @@ public Config(String defaultAppName, File defaultUserDataDir, String... args) { .ofType(boolean.class) .defaultsTo(false); + ArgumentAcceptingOptionSpec bypassMempoolValidationOpt = + parser.accepts(BYPASS_MEMPOOL_VALIDATION, + "Prevents mempool check of trade parameters") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + try { CompositeOptionSet options = new CompositeOptionSet(); @@ -777,6 +786,7 @@ public Config(String defaultAppName, File defaultUserDataDir, String... args) { this.apiPort = options.valueOf(apiPortOpt); this.preventPeriodicShutdownAtSeedNode = options.valueOf(preventPeriodicShutdownAtSeedNodeOpt); this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt); + this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt); } catch (OptionException ex) { throw new ConfigException("problem parsing option '%s': %s", ex.options().get(0), diff --git a/core/src/main/java/bisq/core/app/DomainInitialisation.java b/core/src/main/java/bisq/core/app/DomainInitialisation.java index a5aa26c236c..2abfb966e82 100644 --- a/core/src/main/java/bisq/core/app/DomainInitialisation.java +++ b/core/src/main/java/bisq/core/app/DomainInitialisation.java @@ -39,6 +39,7 @@ import bisq.core.payment.RevolutAccount; import bisq.core.payment.TradeLimits; import bisq.core.provider.fee.FeeService; +import bisq.core.provider.mempool.MempoolService; import bisq.core.provider.price.PriceFeedService; import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; @@ -109,6 +110,7 @@ public class DomainInitialisation { private final User user; private final DaoStateSnapshotService daoStateSnapshotService; private final TriggerPriceService triggerPriceService; + private final MempoolService mempoolService; @Inject public DomainInitialisation(ClockWatcher clockWatcher, @@ -145,7 +147,8 @@ public DomainInitialisation(ClockWatcher clockWatcher, MarketAlerts marketAlerts, User user, DaoStateSnapshotService daoStateSnapshotService, - TriggerPriceService triggerPriceService) { + TriggerPriceService triggerPriceService, + MempoolService mempoolService) { this.clockWatcher = clockWatcher; this.tradeLimits = tradeLimits; this.arbitrationManager = arbitrationManager; @@ -181,6 +184,7 @@ public DomainInitialisation(ClockWatcher clockWatcher, this.user = user; this.daoStateSnapshotService = daoStateSnapshotService; this.triggerPriceService = triggerPriceService; + this.mempoolService = mempoolService; } public void initDomainServices(Consumer rejectedTxErrorMessageHandler, @@ -261,6 +265,7 @@ public void initDomainServices(Consumer rejectedTxErrorMessageHandler, priceAlert.onAllServicesInitialized(); marketAlerts.onAllServicesInitialized(); triggerPriceService.onAllServicesInitialized(); + mempoolService.onAllServicesInitialized(); if (revolutAccountsUpdateHandler != null) { revolutAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream() diff --git a/core/src/main/java/bisq/core/filter/Filter.java b/core/src/main/java/bisq/core/filter/Filter.java index cedc7c4b529..be43c3f7465 100644 --- a/core/src/main/java/bisq/core/filter/Filter.java +++ b/core/src/main/java/bisq/core/filter/Filter.java @@ -98,6 +98,9 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { private final Set nodeAddressesBannedFromNetwork; private final boolean disableApi; + // added at v1.6.0 + private final boolean disableMempoolValidation; + // After we have created the signature from the filter data we clone it and apply the signature static Filter cloneWithSig(Filter filter, String signatureAsBase64) { return new Filter(filter.getBannedOfferIds(), @@ -126,6 +129,7 @@ static Filter cloneWithSig(Filter filter, String signatureAsBase64) { filter.isDisableAutoConf(), filter.getBannedAutoConfExplorers(), filter.getNodeAddressesBannedFromNetwork(), + filter.isDisableMempoolValidation(), filter.isDisableApi()); } @@ -157,6 +161,7 @@ static Filter cloneWithoutSig(Filter filter) { filter.isDisableAutoConf(), filter.getBannedAutoConfExplorers(), filter.getNodeAddressesBannedFromNetwork(), + filter.isDisableMempoolValidation(), filter.isDisableApi()); } @@ -183,6 +188,7 @@ public Filter(List bannedOfferIds, boolean disableAutoConf, List bannedAutoConfExplorers, Set nodeAddressesBannedFromNetwork, + boolean disableMempoolValidation, boolean disableApi) { this(bannedOfferIds, nodeAddressesBannedFromTrading, @@ -210,6 +216,7 @@ public Filter(List bannedOfferIds, disableAutoConf, bannedAutoConfExplorers, nodeAddressesBannedFromNetwork, + disableMempoolValidation, disableApi); } @@ -245,6 +252,7 @@ public Filter(List bannedOfferIds, boolean disableAutoConf, List bannedAutoConfExplorers, Set nodeAddressesBannedFromNetwork, + boolean disableMempoolValidation, boolean disableApi) { this.bannedOfferIds = bannedOfferIds; this.nodeAddressesBannedFromTrading = nodeAddressesBannedFromTrading; @@ -272,6 +280,7 @@ public Filter(List bannedOfferIds, this.disableAutoConf = disableAutoConf; this.bannedAutoConfExplorers = bannedAutoConfExplorers; this.nodeAddressesBannedFromNetwork = nodeAddressesBannedFromNetwork; + this.disableMempoolValidation = disableMempoolValidation; this.disableApi = disableApi; // ownerPubKeyBytes can be null when called from tests @@ -312,6 +321,7 @@ public protobuf.StoragePayload toProtoMessage() { .setDisableAutoConf(disableAutoConf) .addAllBannedAutoConfExplorers(bannedAutoConfExplorers) .addAllNodeAddressesBannedFromNetwork(nodeAddressesBannedFromNetwork) + .setDisableMempoolValidation(disableMempoolValidation) .setDisableApi(disableApi); Optional.ofNullable(signatureAsBase64).ifPresent(builder::setSignatureAsBase64); @@ -352,6 +362,7 @@ public static Filter fromProto(protobuf.Filter proto) { proto.getDisableAutoConf(), ProtoUtil.protocolStringListToList(proto.getBannedAutoConfExplorersList()), ProtoUtil.protocolStringListToSet(proto.getNodeAddressesBannedFromNetworkList()), + proto.getDisableMempoolValidation(), proto.getDisableApi() ); } @@ -396,6 +407,7 @@ public String toString() { ",\n ownerPubKey=" + ownerPubKey + ",\n disableAutoConf=" + disableAutoConf + ",\n nodeAddressesBannedFromNetwork=" + nodeAddressesBannedFromNetwork + + ",\n disableMempoolValidation=" + disableMempoolValidation + ",\n disableApi=" + disableApi + "\n}"; } diff --git a/core/src/main/java/bisq/core/offer/OpenOffer.java b/core/src/main/java/bisq/core/offer/OpenOffer.java index 61ae2f2ad00..4c2d06c3085 100644 --- a/core/src/main/java/bisq/core/offer/OpenOffer.java +++ b/core/src/main/java/bisq/core/offer/OpenOffer.java @@ -73,6 +73,9 @@ public enum State { // If market price reaches that trigger price the offer gets deactivated @Getter private final long triggerPrice; + @Getter + @Setter + transient private long mempoolStatus = -1; public OpenOffer(Offer offer) { this(offer, 0); diff --git a/core/src/main/java/bisq/core/offer/TriggerPriceService.java b/core/src/main/java/bisq/core/offer/TriggerPriceService.java index b4f551969a0..3329843a384 100644 --- a/core/src/main/java/bisq/core/offer/TriggerPriceService.java +++ b/core/src/main/java/bisq/core/offer/TriggerPriceService.java @@ -20,6 +20,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; +import bisq.core.provider.mempool.MempoolService; import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.PriceFeedService; @@ -52,15 +53,18 @@ public class TriggerPriceService { private final P2PService p2PService; private final OpenOfferManager openOfferManager; + private final MempoolService mempoolService; private final PriceFeedService priceFeedService; private final Map> openOffersByCurrency = new HashMap<>(); @Inject public TriggerPriceService(P2PService p2PService, OpenOfferManager openOfferManager, + MempoolService mempoolService, PriceFeedService priceFeedService) { this.p2PService = p2PService; this.openOfferManager = openOfferManager; + this.mempoolService = mempoolService; this.priceFeedService = priceFeedService; } @@ -152,6 +156,20 @@ private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) { openOfferManager.deactivateOpenOffer(openOffer, () -> { }, errorMessage -> { }); + } else if (openOffer.getState() == OpenOffer.State.AVAILABLE) { + // check the mempool if it has not been done before + if (openOffer.getMempoolStatus() < 0 && mempoolService.canRequestBeMade(openOffer.getOffer().getOfferPayload())) { + mempoolService.validateOfferMakerTx(openOffer.getOffer().getOfferPayload(), (txValidator -> { + openOffer.setMempoolStatus(txValidator.isFail() ? 0 : 1); + })); + } + // if the mempool indicated failure then deactivate the open offer + if (openOffer.getMempoolStatus() == 0) { + log.info("Deactivating open offer {} due to mempool validation", openOffer.getOffer().getShortId()); + openOfferManager.deactivateOpenOffer(openOffer, () -> { + }, errorMessage -> { + }); + } } } diff --git a/core/src/main/java/bisq/core/provider/MempoolHttpClient.java b/core/src/main/java/bisq/core/provider/MempoolHttpClient.java new file mode 100644 index 00000000000..18d40016647 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/MempoolHttpClient.java @@ -0,0 +1,45 @@ +/* + * 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.core.provider; + +import bisq.network.Socks5ProxyProvider; +import bisq.network.http.HttpClientImpl; + +import bisq.common.app.Version; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.io.IOException; + +import javax.annotation.Nullable; + +@Singleton +public class MempoolHttpClient extends HttpClientImpl { + @Inject + public MempoolHttpClient(@Nullable Socks5ProxyProvider socks5ProxyProvider) { + super(socks5ProxyProvider); + } + + // returns JSON of the transaction details + public String getTxDetails(String txId) throws IOException { + super.shutDown(); // close any prior incomplete request + String api = "/" + txId; + return get(api, "User-Agent", "bisq/" + Version.VERSION); + } +} diff --git a/core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java b/core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java new file mode 100644 index 00000000000..fcfdeb4a453 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java @@ -0,0 +1,86 @@ +/* + * 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.core.provider.mempool; + +import bisq.core.provider.MempoolHttpClient; +import bisq.core.user.Preferences; + +import bisq.network.Socks5ProxyProvider; + +import bisq.common.util.Utilities; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class MempoolRequest { + private static final ListeningExecutorService executorService = Utilities.getListeningExecutorService("MempoolRequest", 3, 5, 10 * 60); + private final List txBroadcastServices = new ArrayList<>(); + private final MempoolHttpClient mempoolHttpClient; + + public MempoolRequest(Preferences preferences, Socks5ProxyProvider socks5ProxyProvider) { + this.txBroadcastServices.addAll(preferences.getDefaultTxBroadcastServices()); + this.mempoolHttpClient = new MempoolHttpClient(socks5ProxyProvider); + } + + public void getTxStatus(SettableFuture mempoolServiceCallback, String txId) { + mempoolHttpClient.setBaseUrl(getRandomServiceAddress(txBroadcastServices)); + ListenableFuture future = executorService.submit(() -> { + Thread.currentThread().setName("MempoolRequest @ " + mempoolHttpClient.getBaseUrl()); + log.info("Making http request for information on txId: {}", txId); + return mempoolHttpClient.getTxDetails(txId); + }); + + Futures.addCallback(future, new FutureCallback<>() { + public void onSuccess(String mempoolData) { + log.info("Received mempoolData of [{}] from provider", mempoolData); + mempoolServiceCallback.set(mempoolData); + } + public void onFailure(@NotNull Throwable throwable) { + mempoolServiceCallback.setException(throwable); + } + }, MoreExecutors.directExecutor()); + } + + public boolean switchToAnotherProvider() { + txBroadcastServices.remove(mempoolHttpClient.getBaseUrl()); + return txBroadcastServices.size() > 0; + } + + @Nullable + private static String getRandomServiceAddress(List txBroadcastServices) { + List list = checkNotNull(txBroadcastServices); + return !list.isEmpty() ? list.get(new Random().nextInt(list.size())) : null; + } +} + diff --git a/core/src/main/java/bisq/core/provider/mempool/MempoolService.java b/core/src/main/java/bisq/core/provider/mempool/MempoolService.java new file mode 100644 index 00000000000..7894b55f089 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/mempool/MempoolService.java @@ -0,0 +1,275 @@ +/* + * 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.core.provider.mempool; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.DaoStateService; +import bisq.core.filter.FilterManager; +import bisq.core.offer.OfferPayload; +import bisq.core.trade.Trade; +import bisq.core.user.Preferences; + +import bisq.network.Socks5ProxyProvider; + +import bisq.common.UserThread; +import bisq.common.config.Config; + +import org.bitcoinj.core.Coin; + +import com.google.inject.Inject; + +import javax.inject.Singleton; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +@Singleton +public class MempoolService { + private final Socks5ProxyProvider socks5ProxyProvider; + private final Config config; + private final Preferences preferences; + private final FilterManager filterManager; + private final DaoFacade daoFacade; + private final DaoStateService daoStateService; + private final List btcFeeReceivers = new ArrayList<>(); + @Getter + private int outstandingRequests = 0; + + @Inject + public MempoolService(Socks5ProxyProvider socks5ProxyProvider, + Config config, + Preferences preferences, + FilterManager filterManager, + DaoFacade daoFacade, + DaoStateService daoStateService) { + this.socks5ProxyProvider = socks5ProxyProvider; + this.config = config; + this.preferences = preferences; + this.filterManager = filterManager; + this.daoFacade = daoFacade; + this.daoStateService = daoStateService; + } + + public void onAllServicesInitialized() { + btcFeeReceivers.addAll(getAllBtcFeeReceivers()); + } + + public boolean canRequestBeMade() { + return outstandingRequests < 5; // limit max simultaneous lookups + } + + public boolean canRequestBeMade(OfferPayload offerPayload) { + // when validating a new offer, wait 1 block for the tx to propagate + return offerPayload.getBlockHeightAtOfferCreation() < daoStateService.getChainHeight() && canRequestBeMade(); + } + + public void validateOfferMakerTx(OfferPayload offerPayload, Consumer resultHandler) { + validateOfferMakerTx(new TxValidator(daoStateService, offerPayload.getOfferFeePaymentTxId(), Coin.valueOf(offerPayload.getAmount()), + offerPayload.isCurrencyForMakerFeeBtc()), resultHandler); + } + + public void validateOfferMakerTx(TxValidator txValidator, Consumer resultHandler) { + if (!isServiceSupported()) { + UserThread.runAfter(() -> resultHandler.accept(txValidator.endResult("mempool request not supported, bypassing", true)), 1); + return; + } + MempoolRequest mempoolRequest = new MempoolRequest(preferences, socks5ProxyProvider); + validateOfferMakerTx(mempoolRequest, txValidator, resultHandler); + } + + public void validateOfferTakerTx(Trade trade, Consumer resultHandler) { + validateOfferTakerTx(new TxValidator(daoStateService, trade.getTakerFeeTxId(), trade.getTradeAmount(), + trade.isCurrencyForTakerFeeBtc()), resultHandler); + } + + public void validateOfferTakerTx(TxValidator txValidator, Consumer resultHandler) { + if (!isServiceSupported()) { + UserThread.runAfter(() -> resultHandler.accept(txValidator.endResult("mempool request not supported, bypassing", true)), 1); + return; + } + MempoolRequest mempoolRequest = new MempoolRequest(preferences, socks5ProxyProvider); + validateOfferTakerTx(mempoolRequest, txValidator, resultHandler); + } + + public void checkTxIsConfirmed(String txId, Consumer resultHandler) { + TxValidator txValidator = new TxValidator(daoStateService, txId); + if (!isServiceSupported()) { + UserThread.runAfter(() -> resultHandler.accept(txValidator.endResult("mempool request not supported, bypassing", true)), 1); + return; + } + MempoolRequest mempoolRequest = new MempoolRequest(preferences, socks5ProxyProvider); + SettableFuture future = SettableFuture.create(); + Futures.addCallback(future, callbackForTxRequest(mempoolRequest, txValidator, resultHandler), MoreExecutors.directExecutor()); + mempoolRequest.getTxStatus(future, txId); + } + + // /////////////////////////// + + private void validateOfferMakerTx(MempoolRequest mempoolRequest, + TxValidator txValidator, + Consumer resultHandler) { + SettableFuture future = SettableFuture.create(); + Futures.addCallback(future, callbackForMakerTxValidation(mempoolRequest, txValidator, resultHandler), MoreExecutors.directExecutor()); + mempoolRequest.getTxStatus(future, txValidator.getTxId()); + } + + private void validateOfferTakerTx(MempoolRequest mempoolRequest, + TxValidator txValidator, + Consumer resultHandler) { + SettableFuture future = SettableFuture.create(); + Futures.addCallback(future, callbackForTakerTxValidation(mempoolRequest, txValidator, resultHandler), MoreExecutors.directExecutor()); + mempoolRequest.getTxStatus(future, txValidator.getTxId()); + } + + private FutureCallback callbackForMakerTxValidation(MempoolRequest theRequest, + TxValidator txValidator, + Consumer resultHandler) { + outstandingRequests++; + FutureCallback myCallback = new FutureCallback<>() { + @Override + public void onSuccess(@Nullable String jsonTxt) { + UserThread.execute(() -> { + outstandingRequests--; + resultHandler.accept(txValidator.parseJsonValidateMakerFeeTx(jsonTxt, btcFeeReceivers)); + }); + } + + @Override + public void onFailure(Throwable throwable) { + log.warn("onFailure - {}", throwable.toString()); + UserThread.execute(() -> { + outstandingRequests--; + if (theRequest.switchToAnotherProvider()) { + validateOfferMakerTx(theRequest, txValidator, resultHandler); + } else { + // exhausted all providers, let user know of failure + resultHandler.accept(txValidator.endResult("Tx not found", false)); + } + }); + } + }; + return myCallback; + } + + private FutureCallback callbackForTakerTxValidation(MempoolRequest theRequest, + TxValidator txValidator, + Consumer resultHandler) { + outstandingRequests++; + FutureCallback myCallback = new FutureCallback<>() { + @Override + public void onSuccess(@Nullable String jsonTxt) { + UserThread.execute(() -> { + outstandingRequests--; + resultHandler.accept(txValidator.parseJsonValidateTakerFeeTx(jsonTxt, btcFeeReceivers)); + }); + } + + @Override + public void onFailure(Throwable throwable) { + log.warn("onFailure - {}", throwable.toString()); + UserThread.execute(() -> { + outstandingRequests--; + if (theRequest.switchToAnotherProvider()) { + validateOfferTakerTx(theRequest, txValidator, resultHandler); + } else { + // exhausted all providers, let user know of failure + resultHandler.accept(txValidator.endResult("Tx not found", false)); + } + }); + } + }; + return myCallback; + } + + private FutureCallback callbackForTxRequest(MempoolRequest theRequest, + TxValidator txValidator, + Consumer resultHandler) { + outstandingRequests++; + FutureCallback myCallback = new FutureCallback<>() { + @Override + public void onSuccess(@Nullable String jsonTxt) { + UserThread.execute(() -> { + outstandingRequests--; + txValidator.setJsonTxt(jsonTxt); + resultHandler.accept(txValidator); + }); + } + + @Override + public void onFailure(Throwable throwable) { + log.warn("onFailure - {}", throwable.toString()); + UserThread.execute(() -> { + outstandingRequests--; + resultHandler.accept(txValidator.endResult("Tx not found", false)); + }); + + } + }; + return myCallback; + } + + // ///////////////////////////// + + private List getAllBtcFeeReceivers() { + List btcFeeReceivers = new ArrayList<>(); + // fee receivers from filter ref: bisq-network/bisq/pull/4294 + List feeReceivers = Optional.ofNullable(filterManager.getFilter()) + .flatMap(f -> Optional.ofNullable(f.getBtcFeeReceiverAddresses())) + .orElse(List.of()); + feeReceivers.forEach(e -> { + try { + btcFeeReceivers.add(e.split("#")[0]); // victim's receiver address + } catch (RuntimeException ignore) { + // If input format is not as expected we ignore entry + } + }); + btcFeeReceivers.addAll(daoFacade.getAllDonationAddresses()); + log.info("Known BTC fee receivers: {}", btcFeeReceivers.toString()); + + return btcFeeReceivers; + } + + private boolean isServiceSupported() { + if (filterManager.getFilter() != null && filterManager.getFilter().isDisableMempoolValidation()) { + log.info("MempoolService bypassed by filter setting disableMempoolValidation=true"); + return false; + } + if (config.bypassMempoolValidation) { + log.info("MempoolService bypassed by config setting bypassMempoolValidation=true"); + return false; + } + if (!Config.baseCurrencyNetwork().isMainnet()) { + log.info("MempoolService only supports mainnet"); + return false; + } + return true; + } +} diff --git a/core/src/main/java/bisq/core/provider/mempool/TxValidator.java b/core/src/main/java/bisq/core/provider/mempool/TxValidator.java new file mode 100644 index 00000000000..88c2ab47ed3 --- /dev/null +++ b/core/src/main/java/bisq/core/provider/mempool/TxValidator.java @@ -0,0 +1,395 @@ +/* + * 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.core.provider.mempool; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.state.DaoStateService; + +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; + +import java.util.ArrayList; +import java.util.List; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.Nullable; + +import static bisq.core.util.coin.CoinUtil.maxCoin; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +@Getter +public class TxValidator { + private final static double FEE_TOLERANCE = 0.95; // we expect fees to be at least 95% of target + private final static long BLOCK_TOLERANCE = 599999L; // allow really old offers with weird fee addresses + + private final DaoStateService daoStateService; + private final List errorList; + private final String txId; + private Coin amount; + @Nullable + private Boolean isFeeCurrencyBtc = null; + @Nullable + private Long chainHeight; + @Setter + private String jsonTxt; + + + public TxValidator(DaoStateService daoStateService, String txId, Coin amount, @Nullable Boolean isFeeCurrencyBtc) { + this.daoStateService = daoStateService; + this.txId = txId; + this.amount = amount; + this.isFeeCurrencyBtc = isFeeCurrencyBtc; + this.errorList = new ArrayList<>(); + this.jsonTxt = ""; + } + + public TxValidator(DaoStateService daoStateService, String txId) { + this.daoStateService = daoStateService; + this.txId = txId; + this.chainHeight = (long) daoStateService.getChainHeight(); + this.errorList = new ArrayList<>(); + this.jsonTxt = ""; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public TxValidator parseJsonValidateMakerFeeTx(String jsonTxt, List btcFeeReceivers) { + this.jsonTxt = jsonTxt; + boolean status = initialSanityChecks(txId, jsonTxt); + try { + if (status) { + if (checkNotNull(isFeeCurrencyBtc)) { + status = checkFeeAddressBTC(jsonTxt, btcFeeReceivers) + && checkFeeAmountBTC(jsonTxt, amount, true, getBlockHeightForFeeCalculation(jsonTxt)); + } else { + status = checkFeeAmountBSQ(jsonTxt, amount, true, getBlockHeightForFeeCalculation(jsonTxt)); + } + } + } catch (JsonSyntaxException e) { + String s = "The maker fee tx JSON validation failed with reason: " + e.toString(); + log.info(s); + errorList.add(s); + status = false; + } + return endResult("Maker tx validation", status); + } + + public TxValidator parseJsonValidateTakerFeeTx(String jsonTxt, List btcFeeReceivers) { + this.jsonTxt = jsonTxt; + boolean status = initialSanityChecks(txId, jsonTxt); + try { + if (status) { + if (isFeeCurrencyBtc == null) { + isFeeCurrencyBtc = checkFeeAddressBTC(jsonTxt, btcFeeReceivers); + } + if (isFeeCurrencyBtc) { + status = checkFeeAddressBTC(jsonTxt, btcFeeReceivers) + && checkFeeAmountBTC(jsonTxt, amount, false, getBlockHeightForFeeCalculation(jsonTxt)); + } else { + status = checkFeeAmountBSQ(jsonTxt, amount, false, getBlockHeightForFeeCalculation(jsonTxt)); + } + } + } catch (JsonSyntaxException e) { + String s = "The taker fee tx JSON validation failed with reason: " + e.toString(); + log.info(s); + errorList.add(s); + status = false; + } + return endResult("Taker tx validation", status); + } + + public long parseJsonValidateTx() { + if (!initialSanityChecks(txId, jsonTxt)) { + return -1; + } + return getTxConfirms(jsonTxt, chainHeight); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean checkFeeAddressBTC(String jsonTxt, List btcFeeReceivers) { + try { + JsonArray jsonVout = getVinAndVout(jsonTxt).second; + JsonObject jsonVout0 = jsonVout.get(0).getAsJsonObject(); + JsonElement jsonFeeAddress = jsonVout0.get("scriptpubkey_address"); + log.debug("fee address: {}", jsonFeeAddress.getAsString()); + if (btcFeeReceivers.contains(jsonFeeAddress.getAsString())) { + return true; + } else if (getBlockHeightForFeeCalculation(jsonTxt) < BLOCK_TOLERANCE) { + log.info("Leniency rule, unrecognised fee receiver but its a really old offer so let it pass, {}", jsonFeeAddress.getAsString()); + return true; + } else { + String error = "fee address: " + jsonFeeAddress.getAsString() + " was not a known BTC fee receiver"; + errorList.add(error); + log.info(error); + } + } catch (JsonSyntaxException e) { + errorList.add(e.toString()); + log.warn(e.toString()); + } + return false; + } + + private boolean checkFeeAmountBTC(String jsonTxt, Coin tradeAmount, boolean isMaker, long blockHeight) { + JsonArray jsonVin = getVinAndVout(jsonTxt).first; + JsonArray jsonVout = getVinAndVout(jsonTxt).second; + JsonObject jsonVin0 = jsonVin.get(0).getAsJsonObject(); + JsonObject jsonVout0 = jsonVout.get(0).getAsJsonObject(); + JsonElement jsonVIn0Value = jsonVin0.getAsJsonObject("prevout").get("value"); + JsonElement jsonFeeValue = jsonVout0.get("value"); + if (jsonVIn0Value == null || jsonFeeValue == null) { + throw new JsonSyntaxException("vin/vout missing data"); + } + long feeValue = jsonFeeValue.getAsLong(); + log.debug("BTC fee: {}", feeValue); + Coin expectedFee = isMaker ? + getMakerFeeHistorical(true, tradeAmount, blockHeight) : + getTakerFeeHistorical(true, tradeAmount, blockHeight); + double leniencyCalc = feeValue / (double) expectedFee.getValue(); + String description = "Expected BTC fee: " + expectedFee.toString() + " sats , actual fee paid: " + Coin.valueOf(feeValue).toString() + " sats"; + if (expectedFee.getValue() == feeValue) { + log.debug("The fee matched what we expected"); + return true; + } else if (expectedFee.getValue() < feeValue) { + log.info("The fee was more than what we expected: " + description); + return true; + } else if (leniencyCalc > FEE_TOLERANCE) { + log.info("Leniency rule: the fee was low, but above {} of what was expected {} {}", FEE_TOLERANCE, leniencyCalc, description); + return true; + } else { + String feeUnderpaidMessage = "UNDERPAID. " + description; + errorList.add(feeUnderpaidMessage); + log.info(feeUnderpaidMessage); + } + return false; + } + + // I think its better to postpone BSQ fee check once the BSQ trade fee tx is confirmed and then use the BSQ explorer to request the + // BSQ fee to check if it is correct. + // Otherwise the requirements here become very complicated and potentially impossible to verify as we don't know + // if inputs and outputs are valid BSQ without the BSQ parser and confirmed transactions. + private boolean checkFeeAmountBSQ(String jsonTxt, Coin tradeAmount, boolean isMaker, long blockHeight) { + JsonArray jsonVin = getVinAndVout(jsonTxt).first; + JsonArray jsonVout = getVinAndVout(jsonTxt).second; + JsonObject jsonVin0 = jsonVin.get(0).getAsJsonObject(); + JsonObject jsonVout0 = jsonVout.get(0).getAsJsonObject(); + JsonElement jsonVIn0Value = jsonVin0.getAsJsonObject("prevout").get("value"); + JsonElement jsonFeeValue = jsonVout0.get("value"); + if (jsonVIn0Value == null || jsonFeeValue == null) { + throw new JsonSyntaxException("vin/vout missing data"); + } + Coin expectedFee = isMaker ? + getMakerFeeHistorical(false, tradeAmount, blockHeight) : + getTakerFeeHistorical(false, tradeAmount, blockHeight); + long feeValue = jsonVIn0Value.getAsLong() - jsonFeeValue.getAsLong(); + // if the first output (BSQ) is greater than the first input (BSQ) include the second input (presumably BSQ) + if (jsonFeeValue.getAsLong() > jsonVIn0Value.getAsLong()) { + // in this case 2 or more UTXOs were spent to pay the fee: + //TODO missing handling of > 2 BSQ inputs + JsonObject jsonVin1 = jsonVin.get(1).getAsJsonObject(); + JsonElement jsonVIn1Value = jsonVin1.getAsJsonObject("prevout").get("value"); + feeValue += jsonVIn1Value.getAsLong(); + } + log.debug("BURNT BSQ maker fee: {} BSQ ({} sats)", (double) feeValue / 100.0, feeValue); + double leniencyCalc = feeValue / (double) expectedFee.getValue(); + String description = String.format("Expected fee: %.2f BSQ, actual fee paid: %.2f BSQ", + (double) expectedFee.getValue() / 100.0, (double) feeValue / 100.0); + if (expectedFee.getValue() == feeValue) { + log.info("The fee matched what we expected"); + return true; + } else if (expectedFee.getValue() < feeValue) { + log.info("The fee was more than what we expected. " + description); + return true; + } else if (leniencyCalc > FEE_TOLERANCE) { + log.info("Leniency rule: the fee was low, but above {} of what was expected {} {}", FEE_TOLERANCE, leniencyCalc, description); + return true; + } else { + errorList.add(description); + log.info(description); + } + return false; + } + + private static Tuple2 getVinAndVout(String jsonTxt) throws JsonSyntaxException { + // there should always be "vout" at the top level + // check that there are 2 or 3 vout elements: the fee, the reserved for trade, optional change + JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class); + if (json.get("vin") == null || json.get("vout") == null) { + throw new JsonSyntaxException("missing vin/vout"); + } + JsonArray jsonVin = json.get("vin").getAsJsonArray(); + JsonArray jsonVout = json.get("vout").getAsJsonArray(); + if (jsonVin == null || jsonVout == null || jsonVin.size() < 1 || jsonVout.size() < 2) { + throw new JsonSyntaxException("not enough vins/vouts"); + } + return new Tuple2<>(jsonVin, jsonVout); + } + + private static boolean initialSanityChecks(String txId, String jsonTxt) { + // there should always be "status" container element at the top level + if (jsonTxt == null || jsonTxt.length() == 0) { + return false; + } + JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class); + if (json.get("status") == null) { + return false; + } + // there should always be "txid" string element at the top level + if (json.get("txid") == null) { + return false; + } + // txid should match what we requested + if (!txId.equals(json.get("txid").getAsString())) { + return false; + } + JsonObject jsonStatus = json.get("status").getAsJsonObject(); + JsonElement jsonConfirmed = jsonStatus.get("confirmed"); + return (jsonConfirmed != null); + // the json is valid and it contains a "confirmed" field then tx is known to mempool.space + // we don't care if it is confirmed or not, just that it exists. + } + + private static long getTxConfirms(String jsonTxt, long chainHeight) { + long blockHeight = getTxBlockHeight(jsonTxt); + if (blockHeight > 0) { + return (chainHeight - blockHeight) + 1; // if it is in the current block it has 1 conf + } + return 0; // 0 indicates unconfirmed + } + + // we want the block height applicable for calculating the appropriate expected trading fees + // if the tx is not yet confirmed, use current block tip, if tx is confirmed use the block it was confirmed at. + private long getBlockHeightForFeeCalculation(String jsonTxt) { + long txBlockHeight = getTxBlockHeight(jsonTxt); + if (txBlockHeight > 0) { + return txBlockHeight; + } + return daoStateService.getChainHeight(); + } + + // this would be useful for the arbitrator verifying that the delayed payout tx is confirmed + private static long getTxBlockHeight(String jsonTxt) { + // there should always be "status" container element at the top level + JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class); + if (json.get("status") == null) { + return -1L; + } + JsonObject jsonStatus = json.get("status").getAsJsonObject(); + JsonElement jsonConfirmed = jsonStatus.get("confirmed"); + if (jsonConfirmed == null) { + return -1L; + } + if (jsonConfirmed.getAsBoolean()) { + // it is confirmed, lets get the block height + JsonElement jsonBlockHeight = jsonStatus.get("block_height"); + if (jsonBlockHeight == null) { + return -1L; // block height error + } + return (jsonBlockHeight.getAsLong()); + } + return 0L; // in mempool, not confirmed yet + } + + private Coin getMakerFeeHistorical(boolean isFeeCurrencyBtc, Coin amount, long blockHeight) { + double feePerBtcAsDouble; + Coin minMakerFee; + if (isFeeCurrencyBtc) { + feePerBtcAsDouble = (double) getMakerFeeRateBtc(blockHeight).value; + minMakerFee = Coin.valueOf(5000L); // MIN_MAKER_FEE_BTC "0.00005" + } else { + feePerBtcAsDouble = (double) getMakerFeeRateBsq(blockHeight).value; + minMakerFee = Coin.valueOf(3L); // MIN_MAKER_FEE_BSQ "0.03" + } + double amountAsDouble = amount != null ? (double) amount.value : 0; + double btcAsDouble = (double) Coin.COIN.value; + double fact = amountAsDouble / btcAsDouble; + Coin feePerBtc = Coin.valueOf(Math.round(feePerBtcAsDouble * fact)); + return maxCoin(feePerBtc, minMakerFee); + } + + private Coin getTakerFeeHistorical(boolean isFeeCurrencyBtc, Coin amount, long blockHeight) { + double feePerBtcAsDouble; + Coin minTakerFee; + if (isFeeCurrencyBtc) { + feePerBtcAsDouble = (double) getTakerFeeRateBtc(blockHeight).value; + minTakerFee = Coin.valueOf(5000L); // MIN_TAKER_FEE_BTC "0.00005" + } else { + feePerBtcAsDouble = (double) getTakerFeeRateBsq(blockHeight).value; + minTakerFee = Coin.valueOf(3L); // MIN_TAKER_FEE_BSQ "0.03" + } + double amountAsDouble = amount != null ? (double) amount.value : 0; + double btcAsDouble = (double) Coin.COIN.value; + double fact = amountAsDouble / btcAsDouble; + Coin feePerBtc = Coin.valueOf(Math.round(feePerBtcAsDouble * fact)); + return maxCoin(feePerBtc, minTakerFee); + } + + private Coin getMakerFeeRateBsq(long blockHeight) { + return daoStateService.getParamValueAsCoin(Param.DEFAULT_MAKER_FEE_BSQ, (int) blockHeight); + } + + private Coin getTakerFeeRateBsq(long blockHeight) { + return daoStateService.getParamValueAsCoin(Param.DEFAULT_TAKER_FEE_BSQ, (int) blockHeight); + } + + private Coin getMakerFeeRateBtc(long blockHeight) { + return daoStateService.getParamValueAsCoin(Param.DEFAULT_MAKER_FEE_BTC, (int) blockHeight); + } + + private Coin getTakerFeeRateBtc(long blockHeight) { + return daoStateService.getParamValueAsCoin(Param.DEFAULT_TAKER_FEE_BTC, (int) blockHeight); + } + + public TxValidator endResult(String title, boolean status) { + log.info("{} : {}", title, status ? "SUCCESS" : "FAIL"); + if (!status) { + errorList.add(title); + } + return this; + } + + public boolean isFail() { + return errorList.size() > 0; + } + + public boolean getResult() { + return errorList.size() == 0; + } + + public String errorSummary() { + return errorList.toString().substring(0, Math.min(85, errorList.toString().length())); + } + + public String toString() { + return errorList.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/TradeDataValidation.java b/core/src/main/java/bisq/core/trade/TradeDataValidation.java index d0e8cc47c21..013dcdb42ab 100644 --- a/core/src/main/java/bisq/core/trade/TradeDataValidation.java +++ b/core/src/main/java/bisq/core/trade/TradeDataValidation.java @@ -355,6 +355,27 @@ public static void validatePayoutTxInput(Transaction depositTx, } } + public static void validateDepositInputs(Trade trade) throws InvalidTxException { + // assumption: deposit tx always has 2 inputs, the maker and taker + if (trade == null || trade.getDepositTx() == null || trade.getDepositTx().getInputs().size() != 2) { + throw new InvalidTxException("Deposit transaction is null or has unexpected input count"); + } + Transaction depositTx = trade.getDepositTx(); + String txIdInput0 = depositTx.getInput(0).getOutpoint().getHash().toString(); + String txIdInput1 = depositTx.getInput(1).getOutpoint().getHash().toString(); + String contractMakerTxId = trade.getContract().getOfferPayload().getOfferFeePaymentTxId(); + String contractTakerTxId = trade.getContract().getTakerFeeTxID(); + boolean makerFirstMatch = contractMakerTxId.equalsIgnoreCase(txIdInput0) && contractTakerTxId.equalsIgnoreCase(txIdInput1); + boolean takerFirstMatch = contractMakerTxId.equalsIgnoreCase(txIdInput1) && contractTakerTxId.equalsIgnoreCase(txIdInput0); + if (!makerFirstMatch && !takerFirstMatch) { + String errMsg = "Maker/Taker txId in contract does not match deposit tx input"; + log.error(errMsg + + "\nContract Maker tx=" + contractMakerTxId + " Contract Taker tx=" + contractTakerTxId + + "\nDeposit Input0=" + txIdInput0 + " Deposit Input1=" + txIdInput1); + throw new InvalidTxException(errMsg); + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // Exceptions diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index d9b921ab976..74a02f5963d 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -585,10 +585,9 @@ portfolio.tab.editOpenOffer=Edit offer portfolio.closedTrades.deviation.help=Percentage price deviation from market -portfolio.pending.invalidDelayedPayoutTx=There is an issue with a missing or invalid transaction.\n\n\ - Please do NOT send the fiat or altcoin payment. Contact Bisq \ - developers on Keybase [HYPERLINK:https://keybase.io/team/bisq] or on the \ - forum [HYPERLINK:https://bisq.community] for further assistance.\n\n\ +portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\n\ + Please do NOT send the fiat or altcoin payment.\n\n\ + Open a support ticket to get assistance from a Mediator.\n\n\ Error message: {0} portfolio.pending.step1.waitForConf=Wait for blockchain confirmation @@ -2534,6 +2533,9 @@ disputeSummaryWindow.payoutAmount.buyer=Buyer's payout amount disputeSummaryWindow.payoutAmount.seller=Seller's payout amount disputeSummaryWindow.payoutAmount.invert=Use loser as publisher disputeSummaryWindow.reason=Reason of dispute +disputeSummaryWindow.tradePeriodEnd=Trade period end +disputeSummaryWindow.extraInfo=Extra information +disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" @@ -2642,6 +2644,7 @@ filterWindow.add=Add filter filterWindow.remove=Remove filter filterWindow.btcFeeReceiverAddresses=BTC fee receiver addresses filterWindow.disableApi=Disable API +filterWindow.disableMempoolValidation=Disable Mempool Validation offerDetailsWindow.minBtcAmount=Min. BTC amount offerDetailsWindow.min=(min. {0}) @@ -2850,6 +2853,10 @@ popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\n\ Trade ID: {2}.\n\n\ Please open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." +popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n +takeOffer.cancelButton=Cancel take-offer +takeOffer.warningButton=Ignore and continue anyway + # suppress inspection "UnusedProperty" popup.warning.nodeBanned=One of the {0} nodes got banned. # suppress inspection "UnusedProperty" diff --git a/core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java b/core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java new file mode 100644 index 00000000000..e3e25e187da --- /dev/null +++ b/core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java @@ -0,0 +1,278 @@ +/* + * 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.core.provider.mempool; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.state.DaoStateService; +import bisq.core.util.ParsingUtils; +import bisq.core.util.coin.BsqFormatter; + +import com.google.gson.Gson; + +import org.apache.commons.io.IOUtils; + +import org.bitcoinj.core.Coin; + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import org.junit.Test; +import org.junit.Assert; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TxValidatorTest { + private static final Logger log = LoggerFactory.getLogger(TxValidatorTest.class); + + private List btcFeeReceivers = new ArrayList<>(); + + public TxValidatorTest() { + btcFeeReceivers.add("1EKXx73oUhHaUh8JBimtiPGgHfwNmxYKAj"); + btcFeeReceivers.add("1HpvvMHcoXQsX85CjTsco5ZAAMoGu2Mze9"); + btcFeeReceivers.add("3EfRGckBQQuk7cpU7SwatPv8kFD1vALkTU"); + btcFeeReceivers.add("13sxMq8mTw7CTSqgGiMPfwo6ZDsVYrHLmR"); + btcFeeReceivers.add("19qA2BVPoyXDfHKVMovKG7SoxGY7xrBV8c"); + btcFeeReceivers.add("19BNi5EpZhgBBWAt5ka7xWpJpX2ZWJEYyq"); + btcFeeReceivers.add("38bZBj5peYS3Husdz7AH3gEUiUbYRD951t"); + btcFeeReceivers.add("3EtUWqsGThPtjwUczw27YCo6EWvQdaPUyp"); + btcFeeReceivers.add("1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7"); + btcFeeReceivers.add("3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV"); + btcFeeReceivers.add("34VLFgtFKAtwTdZ5rengTT2g2zC99sWQLC"); + log.warn("Known BTC fee receivers: {}", btcFeeReceivers.toString()); + } + + @Test + public void testMakerTx() throws InterruptedException { + String mempoolData, offerData; + + // paid the correct amount of BSQ fees + offerData = "msimscqb,0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b,1000000,10,0,662390"; + mempoolData = "{\"txid\":\"0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":7899}},{\"vout\":2,\"prevout\":{\"value\":54877439}}],\"vout\":[{\"scriptpubkey_address\":\"1FCUu7hqKCSsGhVJaLbGEoCWdZRJRNqq8w\",\"value\":7889},{\"scriptpubkey_address\":\"bc1qkj5l4wxl00ufdx6ygcnrck9fz5u927gkwqcgey\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1qkw4a8u9l5w9fhdh3ue9v7e7celk4jyudzg5gk5\",\"value\":53276799}],\"size\":405,\"weight\":1287,\"fee\":650,\"status\":{\"confirmed\":true,\"block_height\":663140}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateMakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // UNDERPAID expected 1.01 BSQ, actual fee paid 0.80 BSQ (USED 8.00 RATE INSTEAD OF 10.06 RATE) + offerData = "48067552,3b6009da764b71d79a4df8e2d8960b6919cae2e9bdccd5ef281e261fa9cd31b3,10000000,80,0,667656"; + mempoolData = "{\"txid\":\"3b6009da764b71d79a4df8e2d8960b6919cae2e9bdccd5ef281e261fa9cd31b3\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":9717}},{\"vout\":0,\"prevout\":{\"value\":4434912}},{\"vout\":2,\"prevout\":{\"value\":12809932}}],\"vout\":[{\"scriptpubkey_address\":\"1Nzqa4J7ck5bgz7QNXKtcjZExAvReozFo4\",\"value\":9637},{\"scriptpubkey_address\":\"bc1qhmmulf5prreqhccqy2wqpxxn6dcye7ame9dd57\",\"value\":11500000},{\"scriptpubkey_address\":\"bc1qx6hg8km2jdjc5ukhuedmkseu9wlsjtd8zeajpj\",\"value\":5721894}],\"size\":553,\"weight\":1879,\"fee\":23030,\"status\":{\"confirmed\":true,\"block_height\":667660}}"; + Assert.assertFalse(createTxValidator(offerData).parseJsonValidateMakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // UNDERPAID Expected fee: 0.61 BSQ, actual fee paid: 0.35 BSQ (USED 5.75 RATE INSTEAD OF 10.06 RATE) + offerData = "am7DzIv,4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189,6100000,35,0,668195"; + mempoolData = "{\"txid\":\"4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":23893}},{\"vout\":1,\"prevout\":{\"value\":1440000}},{\"vout\":2,\"prevout\":{\"value\":16390881}}],\"vout\":[{\"scriptpubkey_address\":\"1Kmrzq3WGCQsZw5kroEphuk1KgsEr65yB7\",\"value\":23858},{\"scriptpubkey_address\":\"bc1qyw5qql9m7rkse9mhcun225nrjpwycszsa5dpjg\",\"value\":7015000},{\"scriptpubkey_address\":\"bc1q90y3p6mg0pe3rvvzfeudq4mfxafgpc9rulruff\",\"value\":10774186}],\"size\":554,\"weight\":1559,\"fee\":41730,\"status\":{\"confirmed\":true,\"block_height\":668198}}"; + Assert.assertFalse(createTxValidator(offerData).parseJsonValidateMakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // UNDERPAID expected 0.11 BSQ, actual fee paid 0.08 BSQ (USED 5.75 RATE INSTEAD OF 7.53) + offerData = "F1dzaFNQ,f72e263947c9dee6fbe7093fc85be34a149ef5bcfdd49b59b9cc3322fea8967b,1440000,8,0,670822, bsq paid too little"; + mempoolData = "{\"txid\":\"f72e263947c9dee6fbe7093fc85be34a149ef5bcfdd49b59b9cc3322fea8967b\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":15163}},{\"vout\":2,\"prevout\":{\"value\":6100000}}],\"vout\":[{\"scriptpubkey_address\":\"1MEsc2m4MSomNJWSr1p6fhnUQMyA3DRGrN\",\"value\":15155},{\"scriptpubkey_address\":\"bc1qztgwe9ry9a9puchjuscqdnv4v9lsm2ut0jtfec\",\"value\":2040000},{\"scriptpubkey_address\":\"bc1q0nstwxc0vqkj4x000xt328mfjapvlsd56nn70h\",\"value\":4048308}],\"size\":406,\"weight\":1291,\"fee\":11700,\"status\":{\"confirmed\":true,\"block_height\":670823}}"; + Assert.assertFalse(createTxValidator(offerData).parseJsonValidateMakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + } + + @Test + public void testTakerTx() throws InterruptedException { + String mempoolData, offerData; + + // The fee was more than what we expected: Expected BTC fee: 5000 sats , actual fee paid: 6000 sats + offerData = "00072328,3524364062c96ba0280621309e8b539d152154422294c2cf263a965dcde9a8ca,1000000,6000,1,614672"; + mempoolData = "{\"txid\":\"3524364062c96ba0280621309e8b539d152154422294c2cf263a965dcde9a8ca\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":1,\"prevout\":{\"value\":2971000}}],\"vout\":[{\"scriptpubkey_address\":\"3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV\",\"value\":6000},{\"scriptpubkey_address\":\"1Hxu2X9Nr2fT3qEk9yjhiF54TJEz1Cxjoa\",\"value\":1607600},{\"scriptpubkey_address\":\"16VP6nHDDkmCMwaJj4PeyVHB88heDdVu9e\",\"value\":1353600}],\"size\":257,\"weight\":1028,\"fee\":3800,\"status\":{\"confirmed\":true,\"block_height\":614672}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // The fee matched what we expected + offerData = "00072328,12f658954890d38ce698355be0b27fdd68d092c7b1b7475381918db060f46166,6250000,188,0,615955"; + mempoolData = "{\"txid\":\"12f658954890d38ce698355be0b27fdd68d092c7b1b7475381918db060f46166\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":19980}},{\"vout\":2,\"prevout\":{\"value\":2086015}},{\"vout\":0,\"prevout\":{\"value\":1100000}},{\"vout\":2,\"prevout\":{\"value\":938200}}],\"vout\":[{\"scriptpubkey_address\":\"17qiF1TYgT1YvsCPJyXQoKMtBZ7YJBW9GH\",\"value\":19792},{\"scriptpubkey_address\":\"16aFKD5hvEjJgPme5yRNJT2rAPdTXzdQc2\",\"value\":3768432},{\"scriptpubkey_address\":\"1D5V3QW8f5n4PhwfPgNkW9eWZwNJFyVU8n\",\"value\":346755}],\"size\":701,\"weight\":2804,\"fee\":9216,\"status\":{\"confirmed\":true,\"block_height\":615955}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // The fee was more than what we expected: Expected BTC fee: 5000 sats , actual fee paid: 7000 sats + offerData = "bsqtrade,dfa4555ab78c657cad073e3f29c38c563d9dafc53afaa8c6af28510c734305c4,1000000,10,1,662390"; + mempoolData = "{\"txid\":\"dfa4555ab78c657cad073e3f29c38c563d9dafc53afaa8c6af28510c734305c4\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":678997}}],\"vout\":[{\"scriptpubkey_address\":\"3EfRGckBQQuk7cpU7SwatPv8kFD1vALkTU\",\"value\":7000},{\"scriptpubkey_address\":\"bc1qu6vey3e7flzg8gmhun05m43uc2vz0ay33kuu6r\",\"value\":647998}],\"size\":224,\"weight\":566,\"fee\":23999,\"status\":{\"confirmed\":true,\"block_height\":669720}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // The fee matched what we expected + offerData = "89284,e1269aad63b3d894f5133ad658960971ef5c0fce6a13ad10544dc50fa3360588,900000,9,0,666473"; + mempoolData = "{\"txid\":\"e1269aad63b3d894f5133ad658960971ef5c0fce6a13ad10544dc50fa3360588\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":72738}},{\"vout\":0,\"prevout\":{\"value\":1600000}}],\"vout\":[{\"scriptpubkey_address\":\"17Kh5Ype9yNomqRrqu2k1mdV5c6FcKfGwQ\",\"value\":72691},{\"scriptpubkey_address\":\"bc1qdr9zcw7gf2sehxkux4fmqujm5uguhaqz7l9lca\",\"value\":629016},{\"scriptpubkey_address\":\"bc1qgqrrqv8q6l5d3t52fe28ghuhz4xqrsyxlwn03z\",\"value\":956523}],\"size\":404,\"weight\":1286,\"fee\":14508,\"status\":{\"confirmed\":true,\"block_height\":672388}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + + // UNDERPAID: Expected fee: 7.04 BSQ, actual fee paid: 1.01 BSQ + offerData = "VOxRS,e99ea06aefc824fd45031447f7a0b56efb8117a09f9b8982e2c4da480a3a0e91,10000000,101,0,669129"; + mempoolData = "{\"txid\":\"e99ea06aefc824fd45031447f7a0b56efb8117a09f9b8982e2c4da480a3a0e91\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":16739}},{\"vout\":2,\"prevout\":{\"value\":113293809}}],\"vout\":[{\"scriptpubkey_address\":\"1F14nF6zoUfJkqZrFgdmK5VX5QVwEpAnKW\",\"value\":16638},{\"scriptpubkey_address\":\"bc1q80y688ev7u43vqy964yf7feqddvt2mkm8977cm\",\"value\":11500000},{\"scriptpubkey_address\":\"bc1q9whgyc2du9mrgnxz0nl0shwpw8ugrcae0j0w8p\",\"value\":101784485}],\"size\":406,\"weight\":1291,\"fee\":9425,\"status\":{\"confirmed\":true,\"block_height\":669134}}"; + Assert.assertFalse(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + } + + @Test + public void testGoodOffers() throws InterruptedException { + Map goodOffers = loadJsonTestData("offerTestData.json"); + Map mempoolData = loadJsonTestData("txInfo.json"); + Assert.assertTrue(goodOffers.size() > 0); + Assert.assertTrue(mempoolData.size() > 0); + log.warn("TESTING GOOD OFFERS"); + testOfferSet(goodOffers, mempoolData, true); + } + + @Test + public void testBadOffers() throws InterruptedException { + Map badOffers = loadJsonTestData("badOfferTestData.json"); + Map mempoolData = loadJsonTestData("txInfo.json"); + Assert.assertTrue(badOffers.size() > 0); + Assert.assertTrue(mempoolData.size() > 0); + log.warn("TESTING BAD OFFERS"); + testOfferSet(badOffers, mempoolData, false); + } + + private void testOfferSet(Map offers, Map mempoolData, boolean expectedResult) { + Set knownValuesList = new HashSet<>(offers.values()); + knownValuesList.forEach(offerData -> { + TxValidator txValidator = createTxValidator(offerData); + log.warn("TESTING {}", txValidator.getTxId()); + String jsonTxt = mempoolData.get(txValidator.getTxId()); + if (jsonTxt == null || jsonTxt.isEmpty()) { + log.warn("{} was not found in the mempool", txValidator.getTxId()); + Assert.assertFalse(expectedResult); // tx was not found in explorer + } else { + txValidator.parseJsonValidateMakerFeeTx(jsonTxt, btcFeeReceivers); + Assert.assertTrue(expectedResult == txValidator.getResult()); + } + }); + } + + private Map loadJsonTestData(String fileName) { + String json = ""; + try { + json = IOUtils.toString(this.getClass().getResourceAsStream(fileName), "UTF-8"); + } catch (IOException e) { + log.error(e.toString()); + } + Map map = new Gson().fromJson(json, Map.class); + return map; + } + + // initialize the TxValidator with offerData to be validated + // and mock the used DaoStateService + private TxValidator createTxValidator(String offerData) { + try { + String[] y = offerData.split(","); + String txId = y[1]; + long amount = Long.parseLong(y[2]); + boolean isCurrencyForMakerFeeBtc = Long.parseLong(y[4]) > 0; + DaoStateService mockedDaoStateService = mock(DaoStateService.class); + + Answer mockGetMakerFeeBsq = invocation -> { + return mockedGetMakerFeeBsq(invocation.getArgument(1)); + }; + Answer mockGetTakerFeeBsq = invocation -> { + return mockedGetTakerFeeBsq(invocation.getArgument(1)); + }; + Answer mockGetMakerFeeBtc = invocation -> { + return mockedGetMakerFeeBtc(invocation.getArgument(1)); + }; + Answer mockGetTakerFeeBtc = invocation -> { + return mockedGetTakerFeeBtc(invocation.getArgument(1)); + }; + when(mockedDaoStateService.getParamValueAsCoin(Mockito.same(Param.DEFAULT_MAKER_FEE_BSQ), Mockito.anyInt())).thenAnswer(mockGetMakerFeeBsq); + when(mockedDaoStateService.getParamValueAsCoin(Mockito.same(Param.DEFAULT_TAKER_FEE_BSQ), Mockito.anyInt())).thenAnswer(mockGetTakerFeeBsq); + when(mockedDaoStateService.getParamValueAsCoin(Mockito.same(Param.DEFAULT_MAKER_FEE_BTC), Mockito.anyInt())).thenAnswer(mockGetMakerFeeBtc); + when(mockedDaoStateService.getParamValueAsCoin(Mockito.same(Param.DEFAULT_TAKER_FEE_BTC), Mockito.anyInt())).thenAnswer(mockGetTakerFeeBtc); + TxValidator txValidator = new TxValidator(mockedDaoStateService, txId, Coin.valueOf(amount), isCurrencyForMakerFeeBtc); + return txValidator; + } catch (RuntimeException ignore) { + // If input format is not as expected we ignore entry + } + return null; + } + + // for testing purposes, we have a hardcoded list of needed DAO param values + // since we cannot start the P2P network / DAO in order to run tests + Coin mockedGetMakerFeeBsq(int blockHeight) { + BsqFormatter bsqFormatter = new BsqFormatter(); + LinkedHashMap feeMap = new LinkedHashMap<>(); + feeMap.put(670027L, "7.53"); + feeMap.put(660667L, "10.06"); + feeMap.put(655987L, "8.74"); + feeMap.put(641947L, "7.6"); + feeMap.put(632587L, "6.6"); + feeMap.put(623227L, "5.75"); + feeMap.put(599827L, "10.0"); + feeMap.put(590467L, "13.0"); + feeMap.put(585787L, "8.0"); + feeMap.put(581107L, "1.6"); + for (Map.Entry entry : feeMap.entrySet()) { + if (blockHeight >= entry.getKey()) { + return ParsingUtils.parseToCoin(entry.getValue(), bsqFormatter); + } + } + return ParsingUtils.parseToCoin("0.5", bsqFormatter); // DEFAULT_MAKER_FEE_BSQ("0.50", ParamType.BSQ, 5, 5), // ~ 0.01% of trade amount + } + + Coin mockedGetTakerFeeBsq(int blockHeight) { + BsqFormatter bsqFormatter = new BsqFormatter(); + LinkedHashMap feeMap = new LinkedHashMap<>(); + feeMap.put(670027L, "52.68"); + feeMap.put(660667L, "70.39"); + feeMap.put(655987L, "61.21"); + feeMap.put(641947L, "53.23"); + feeMap.put(632587L, "46.30"); + feeMap.put(623227L, "40.25"); + feeMap.put(599827L, "30.00"); + feeMap.put(590467L, "38.00"); + feeMap.put(585787L, "24.00"); + feeMap.put(581107L, "4.80"); + for (Map.Entry entry : feeMap.entrySet()) { + if (blockHeight >= entry.getKey()) { + return ParsingUtils.parseToCoin(entry.getValue(), bsqFormatter); + } + } + return ParsingUtils.parseToCoin("1.5", bsqFormatter); + } + + Coin mockedGetMakerFeeBtc(int blockHeight) { + BsqFormatter bsqFormatter = new BsqFormatter(); + LinkedHashMap feeMap = new LinkedHashMap<>(); + feeMap.put(623227L, "0.0010"); + feeMap.put(585787L, "0.0020"); + for (Map.Entry entry : feeMap.entrySet()) { + if (blockHeight >= entry.getKey()) { + return ParsingUtils.parseToCoin(entry.getValue(), bsqFormatter); + } + } + return ParsingUtils.parseToCoin("0.001", bsqFormatter); + } + + Coin mockedGetTakerFeeBtc(int blockHeight) { + BsqFormatter bsqFormatter = new BsqFormatter(); + LinkedHashMap feeMap = new LinkedHashMap<>(); + feeMap.put(623227L, "0.0070"); + feeMap.put(585787L, "0.0060"); + for (Map.Entry entry : feeMap.entrySet()) { + if (blockHeight >= entry.getKey()) { + return ParsingUtils.parseToCoin(entry.getValue(), bsqFormatter); + } + } + return ParsingUtils.parseToCoin("0.003", bsqFormatter); + } + +} diff --git a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java index 7241ad3a4b2..7d98e959238 100644 --- a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java +++ b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java @@ -68,6 +68,7 @@ public void testRoundtripFull() { false, Lists.newArrayList(), new HashSet<>(), + false, false)); vo.setRegisteredArbitrator(ArbitratorTest.getArbitratorMock()); diff --git a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java index 731dcfb6cc4..32632190c6d 100644 --- a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java +++ b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java @@ -126,6 +126,7 @@ private static Filter filterWithReceivers(List btcFeeReceiverAddresses) false, Lists.newArrayList(), new HashSet<>(), + false, false); } } diff --git a/core/src/test/resources/bisq/core/provider/mempool/badOfferTestData.json b/core/src/test/resources/bisq/core/provider/mempool/badOfferTestData.json new file mode 100644 index 00000000000..2d9601aa137 --- /dev/null +++ b/core/src/test/resources/bisq/core/provider/mempool/badOfferTestData.json @@ -0,0 +1,13 @@ +{ + "ef1ea38b46402deb7df08c13a6dc379a65542a6940ac9d4ba436641ffd4bcb6e": "FQ0A7G,ef1ea38b46402deb7df08c13a6dc379a65542a6940ac9d4ba436641ffd4bcb6e,15970000,92,0,640438, expected 1.05 actual 0.92 BSQ", + "4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189": "am7DzIv,4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189,6100000,35,0,668195, ", + "051770f8d7f43a9b6ca10fefa6cdf4cb124a81eed26dc8af2e40f57d2589107b": "046698,051770f8d7f43a9b6ca10fefa6cdf4cb124a81eed26dc8af2e40f57d2589107b,15970000,92,0,667927, bsq fee underpaid using 5.75 rate for some weird reason", + "37fba8bf119c289481eef031c0a35e126376f71d13d7cce35eb0d5e05799b5da": "hUWPf,37fba8bf119c289481eef031c0a35e126376f71d13d7cce35eb0d5e05799b5da,19910000,200,0,668994, tx_missing_from_blockchain_for_4_days", + "b3bc726aa2aa6533cb1e61901ce351eecde234378fe650aee267388886aa6e4b": "ebdttmzh,b3bc726aa2aa6533cb1e61901ce351eecde234378fe650aee267388886aa6e4b,4000000,5000,1,669137, tx_missing_from_blockchain_for_2_days", + "10f32fe53081466f003185a9ef0324d6cbe3f59334ee9ccb2f7155cbfad9c1de": "kmbyoexc,10f32fe53081466f003185a9ef0324d6cbe3f59334ee9ccb2f7155cbfad9c1de,33000000,332,0,668954, tx_not_found", + "cd99836ac4246c3e3980edf95773060481ce52271b74dadeb41e18c42ed21188": "nlaIlAvE,cd99836ac4246c3e3980edf95773060481ce52271b74dadeb41e18c42ed21188,5000000,546,1,669262, invalid_missing_fee_address", + "fc3cb16293895fea8ea5d2d8ab4e39d1b27f583e2c160468b586789a861efa74": "feescammer,fc3cb16293895fea8ea5d2d8ab4e39d1b27f583e2c160468b586789a861efa74,1000000,546,1,669442, invalid_missing_fee_address", + "72cabb5c323c923b43c7f6551974f591dcee148778ee34f9131011ea0ca82813": "PBFICEAS,72cabb5c323c923b43c7f6551974f591dcee148778ee34f9131011ea0ca82813,2000000,546,1,672969, dust_fee_scammer", + "1c8e4934f93b5bbd2823318d5d491698316216f2e4bc0d7cd353f6b16358d80e": "feescammer,1c8e4934f93b5bbd2823318d5d491698316216f2e4bc0d7cd353f6b16358d80e,2000000,546,1,669227, dust_fee_scammer", + "17cbd95d8809dc8808a5c209208f59c4a80e09e012e62951668d30d716c44a96": "feescammer,17cbd95d8809dc8808a5c209208f59c4a80e09e012e62951668d30d716c44a96,2000000,546,1,669340, dust_fee_scammer" +} diff --git a/core/src/test/resources/bisq/core/provider/mempool/offerTestData.json b/core/src/test/resources/bisq/core/provider/mempool/offerTestData.json new file mode 100644 index 00000000000..4dc54de7bcd --- /dev/null +++ b/core/src/test/resources/bisq/core/provider/mempool/offerTestData.json @@ -0,0 +1,19 @@ +{ + "e125fdbd09ee86c01e16e1f12a31507cfb8703ed1bd5a221461adf33cb3e00d9": "7213472,e125fdbd09ee86c01e16e1f12a31507cfb8703ed1bd5a221461adf33cb3e00d9,200000000,200000,1,578672, unknown_fee_receiver_1PUXU1MQ", + "44b00de808d0145f9a948fe1b020c5d4173402ba0b5a5ba69124c67e371bca18": "aAPLmh98,44b00de808d0145f9a948fe1b020c5d4173402ba0b5a5ba69124c67e371bca18,140000000,140000,1,578629, unknown_fee_receiver_1PUXU1MQ", + "654a7a34321b57be6a553052d1d9e0f1764dd2fab7b64c9422e9953e4d9d127d": "pwdbdku,654a7a34321b57be6a553052d1d9e0f1764dd2fab7b64c9422e9953e4d9d127d,24980000,238000,1,554947, unknown_fee_receiver_18GzH11", + "0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b": "msimscqb,0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b,1000000,10,0,662390", + "2861f4526f40686d5cddc364035b561c13625996233a8b8705195041504ba3a1": "89284,2861f4526f40686d5cddc364035b561c13625996233a8b8705195041504ba3a1,900000,9,0,666473", + "a571e8a2e9227025f897017db0a7cbd7baea98d7f119aea49c46d6535d79caba": "EHGVHSL,a571e8a2e9227025f897017db0a7cbd7baea98d7f119aea49c46d6535d79caba,1000000,5000,1,665825", + "ac001c7eff1cfaaf45f955a9a353f113153cd21610e5e8449b15559592b25d6e": "M2CNGNN,ac001c7eff1cfaaf45f955a9a353f113153cd21610e5e8449b15559592b25d6e,600000,6,0,669043", + "cdd49f58806253abfa6f6566d0659c2f51c28256ef19acdde6a23331d6f07348": "qHBsg,cdd49f58806253abfa6f6566d0659c2f51c28256ef19acdde6a23331d6f07348,25840000,258,0,611324", + "aaf29059ba14264d9fa85fe6700c13b36b3b4aa2748745eafefabcf276dc2c25": "87822,aaf29059ba14264d9fa85fe6700c13b36b3b4aa2748745eafefabcf276dc2c25,1000000,10,0,668839", + "9ab825e4eb298ceb74237faf576c0f5088430fdf05e61a4e9ae4028508b318e6": "9134295,9ab825e4eb298ceb74237faf576c0f5088430fdf05e61a4e9ae4028508b318e6,30000000,30000,1,666606", + "768df499434d48f6dc3329e0abfd3bbc930b884b2caff9b7e4d7f1ec15c4c28d": "5D4EQC,768df499434d48f6dc3329e0abfd3bbc930b884b2caff9b7e4d7f1ec15c4c28d,10000000,101,0,668001", + "9b517de9ef9c00a779b58271a301347e13fc9525be42100452f462526a6f8523": "23608,9b517de9ef9c00a779b58271a301347e13fc9525be42100452f462526a6f8523,5000000,5000,1,668593", + "02f8976ca80f98f095c5656675aa6f40aafced65451917443c1e5057186f2592": "I3WzjuF,02f8976ca80f98f095c5656675aa6f40aafced65451917443c1e5057186f2592,1000000,10,0,666563", + "995de91e69e2590aff67ae6e4f2d417bad6882b11cc095b2420fef7506209be8": "WlvThoI,995de91e69e2590aff67ae6e4f2d417bad6882b11cc095b2420fef7506209be8,1000000,5000,1,669231", + "ca4a1f991c3f585e4fbbb5b5aeb0766ba3eb46bb1c3ff3714db7a8cadd0e557d": "ffhpgz0z,ca4a1f991c3f585e4fbbb5b5aeb0766ba3eb46bb1c3ff3714db7a8cadd0e557d,2000000,20,0,667351", + "b9e1a791f4091910caeb70d1a5d56452bc9614c16b5b74281b2485551faeb46e": "jgtwzsn,b9e1a791f4091910caeb70d1a5d56452bc9614c16b5b74281b2485551faeb46e,1000000,10,0,666372", + "dc06cd41f4b778553a0a5df4578c62eeb0c9c878b6f1a24c60b619a6749877c7": "AZhkSO,dc06cd41f4b778553a0a5df4578c62eeb0c9c878b6f1a24c60b619a6749877c7,200000000,200000,1,668526" +} diff --git a/core/src/test/resources/bisq/core/provider/mempool/txInfo.json b/core/src/test/resources/bisq/core/provider/mempool/txInfo.json new file mode 100644 index 00000000000..9c72bbc177d --- /dev/null +++ b/core/src/test/resources/bisq/core/provider/mempool/txInfo.json @@ -0,0 +1,27 @@ +{ + "44b00de808d0145f9a948fe1b020c5d4173402ba0b5a5ba69124c67e371bca18": "{\"txid\":\"44b00de808d0145f9a948fe1b020c5d4173402ba0b5a5ba69124c67e371bca18\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":1,\"prevout\":{\"value\":147186800}}],\"vout\":[{\"scriptpubkey_address\":\"1PUXU1MQ82JC3Hx1NN5tZs3BaTAJVg72MC\",\"value\":140000},{\"scriptpubkey_address\":\"1HwN7DhxNQdFKzMbrQq5vRHzY4xXGTRcne\",\"value\":147000000}],\"size\":226,\"weight\":904,\"fee\":46800,\"status\":{\"confirmed\":true,\"block_height\":578630}}", + "2861f4526f40686d5cddc364035b561c13625996233a8b8705195041504ba3a1": "{\"txid\":\"2861f4526f40686d5cddc364035b561c13625996233a8b8705195041504ba3a1\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":1393}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":2,\"prevout\":{\"value\":10872788}}],\"vout\":[{\"scriptpubkey_address\":\"1NsTgbTUKhveanGCmsawJKLf6asQhJP4p2\",\"value\":1384},{\"scriptpubkey_address\":\"bc1qlw44hxyqfwcmcuuvtktduhth5ah4djl63sc4eq\",\"value\":1500000},{\"scriptpubkey_address\":\"bc1qyty4urzh25j5qypqu7v9mzhwt3p0zvaxeehpxp\",\"value\":9967337}],\"size\":552,\"weight\":1557,\"fee\":5460,\"status\":{\"confirmed\":true,\"block_height\":666479}}", + "0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b": "{\"txid\":\"0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":7899}},{\"vout\":2,\"prevout\":{\"value\":54877439}}],\"vout\":[{\"scriptpubkey_address\":\"1FCUu7hqKCSsGhVJaLbGEoCWdZRJRNqq8w\",\"value\":7889},{\"scriptpubkey_address\":\"bc1qkj5l4wxl00ufdx6ygcnrck9fz5u927gkwqcgey\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1qkw4a8u9l5w9fhdh3ue9v7e7celk4jyudzg5gk5\",\"value\":53276799}],\"size\":405,\"weight\":1287,\"fee\":650,\"status\":{\"confirmed\":true,\"block_height\":663140}}", + "a571e8a2e9227025f897017db0a7cbd7baea98d7f119aea49c46d6535d79caba": "{\"txid\":\"a571e8a2e9227025f897017db0a7cbd7baea98d7f119aea49c46d6535d79caba\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":798055}}],\"vout\":[{\"scriptpubkey_address\":\"38bZBj5peYS3Husdz7AH3gEUiUbYRD951t\",\"value\":5000},{\"scriptpubkey_address\":\"bc1qy69ekanm2twzqqr7vz9qcxypyta29wdm2t0ay8\",\"value\":600000},{\"scriptpubkey_address\":\"bc1qp6q2urrntp8tq67lhymftsq0dpqvqmpnus7hym\",\"value\":184830}],\"size\":254,\"weight\":689,\"fee\":8225,\"status\":{\"confirmed\":true,\"block_height\":665826}}", + "ac001c7eff1cfaaf45f955a9a353f113153cd21610e5e8449b15559592b25d6e": "{\"txid\":\"ac001c7eff1cfaaf45f955a9a353f113153cd21610e5e8449b15559592b25d6e\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":4165}},{\"vout\":2,\"prevout\":{\"value\":593157}},{\"vout\":2,\"prevout\":{\"value\":595850}}],\"vout\":[{\"scriptpubkey_address\":\"16Y1WqYEbWygHz6kuhJxWXos3bw46JNNoZ\",\"value\":4159},{\"scriptpubkey_address\":\"bc1qkxjvjp2hyegjpw5jtlju7fcr7pv9en3u7cg7q7\",\"value\":600000},{\"scriptpubkey_address\":\"bc1q9x95y8ktsxg9jucky66da3v2s2har56cy3nkkg\",\"value\":575363}],\"size\":555,\"weight\":1563,\"fee\":13650,\"status\":{\"confirmed\":true,\"block_height\":669045}}", + "cdd49f58806253abfa6f6566d0659c2f51c28256ef19acdde6a23331d6f07348": "{\"txid\":\"cdd49f58806253abfa6f6566d0659c2f51c28256ef19acdde6a23331d6f07348\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":4898}},{\"vout\":2,\"prevout\":{\"value\":15977562}}],\"vout\":[{\"scriptpubkey_address\":\"16SCUfnCLddxgoAYLUcsJcoE4VxBRzgTSz\",\"value\":4640},{\"scriptpubkey_address\":\"1N9Pb6DTJXh96QjzYLDFTZuBvFXgFPi18N\",\"value\":1292000},{\"scriptpubkey_address\":\"1C7tg4KT9wQvLR5xfPDqD4U35Ncwk3UQxm\",\"value\":14681720}],\"size\":406,\"weight\":1624,\"fee\":4100,\"status\":{\"confirmed\":true,\"block_height\":611325}}", + "aaf29059ba14264d9fa85fe6700c13b36b3b4aa2748745eafefabcf276dc2c25": "{\"txid\":\"aaf29059ba14264d9fa85fe6700c13b36b3b4aa2748745eafefabcf276dc2c25\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":3512}},{\"vout\":2,\"prevout\":{\"value\":1481349}},{\"vout\":1,\"prevout\":{\"value\":600000}}],\"vout\":[{\"scriptpubkey_address\":\"14rNP2aC23hr6u8ALmksm3RgJys7CAD3No\",\"value\":3502},{\"scriptpubkey_address\":\"bc1qvctcjcrhznptmydv4hxwc4wd2km76shkl3jj29\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1qsdzpvr6sehypswcwjsmmjzctjhy5hkwqvf2vh8\",\"value\":476289}],\"size\":555,\"weight\":1563,\"fee\":5070,\"status\":{\"confirmed\":true,\"block_height\":668841}}", + "9ab825e4eb298ceb74237faf576c0f5088430fdf05e61a4e9ae4028508b318e6": "{\"txid\":\"9ab825e4eb298ceb74237faf576c0f5088430fdf05e61a4e9ae4028508b318e6\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":4533500}}],\"vout\":[{\"scriptpubkey_address\":\"1EKXx73oUhHaUh8JBimtiPGgHfwNmxYKAj\",\"value\":30000},{\"scriptpubkey_address\":\"bc1qde7asrvrnkn5st5q8u038fxt9tlrgyaxwju6hn\",\"value\":4500000}],\"size\":226,\"weight\":574,\"fee\":3500,\"status\":{\"confirmed\":true,\"block_height\":666607}}", + "768df499434d48f6dc3329e0abfd3bbc930b884b2caff9b7e4d7f1ec15c4c28d": "{\"txid\":\"768df499434d48f6dc3329e0abfd3bbc930b884b2caff9b7e4d7f1ec15c4c28d\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":249699}},{\"vout\":1,\"prevout\":{\"value\":4023781}}],\"vout\":[{\"scriptpubkey_address\":\"1J8wtmJuurfBSrRb27urtoHzuQey1ipXPX\",\"value\":249598},{\"scriptpubkey_address\":\"bc1qfmyw7pwaqucprcsauqr6gvez9wep290r4amd3y\",\"value\":1500000},{\"scriptpubkey_address\":\"bc1q2lx0fymd3mmk4pzjq2k8hn7mk3hnctnjtu497t\",\"value\":2517382}],\"size\":405,\"weight\":1287,\"fee\":6500,\"status\":{\"confirmed\":true,\"block_height\":668002}}", + "02f8976ca80f98f095c5656675aa6f40aafced65451917443c1e5057186f2592": "{\"txid\":\"02f8976ca80f98f095c5656675aa6f40aafced65451917443c1e5057186f2592\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":33860}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":2,\"prevout\":{\"value\":39304}},{\"vout\":3,\"prevout\":{\"value\":5000000}}],\"vout\":[{\"scriptpubkey_address\":\"1Le1auzXSpEnyMc6S9KNentnye3gTPLnuA\",\"value\":33850},{\"scriptpubkey_address\":\"bc1qs73jfmjzclsx9466pvpslfuqc2kkv5uc8u928a\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1q85zlv50mddyuerze7heve0vcv4f80qsw2szv34\",\"value\":4031088}],\"size\":701,\"weight\":1829,\"fee\":8226,\"status\":{\"confirmed\":true,\"block_height\":666564}}", + "9b517de9ef9c00a779b58271a301347e13fc9525be42100452f462526a6f8523": "{\"txid\":\"9b517de9ef9c00a779b58271a301347e13fc9525be42100452f462526a6f8523\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":4466600}}],\"vout\":[{\"scriptpubkey_address\":\"38bZBj5peYS3Husdz7AH3gEUiUbYRD951t\",\"value\":5000},{\"scriptpubkey_address\":\"bc1qrl0dvwp6hpqlcj65qfhl70lz67yjvhlc8z73a4\",\"value\":750000},{\"scriptpubkey_address\":\"bc1qg55gnkhgg4zltdh76sdef33xzr7h95g3xsxesg\",\"value\":3709675}],\"size\":254,\"weight\":689,\"fee\":1925,\"status\":{\"confirmed\":true,\"block_height\":668843}}", + "995de91e69e2590aff67ae6e4f2d417bad6882b11cc095b2420fef7506209be8": "{\"txid\":\"995de91e69e2590aff67ae6e4f2d417bad6882b11cc095b2420fef7506209be8\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":36006164}}],\"vout\":[{\"scriptpubkey_address\":\"19qA2BVPoyXDfHKVMovKG7SoxGY7xrBV8c\",\"value\":5000},{\"scriptpubkey_address\":\"bc1qefxyxsq9tskaw0qarxf0hdxusxe64l8zsmsgrz\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1q6fky0fxcg3zrz5t0xdyq5sh90h7m5sya0wf9gx\",\"value\":34390664}],\"size\":256,\"weight\":697,\"fee\":10500,\"status\":{\"confirmed\":true,\"block_height\":669233}}", + "fc3cb16293895fea8ea5d2d8ab4e39d1b27f583e2c160468b586789a861efa74": "{\"txid\":\"fc3cb16293895fea8ea5d2d8ab4e39d1b27f583e2c160468b586789a861efa74\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":1,\"prevout\":{\"value\":49144}},{\"vout\":0,\"prevout\":{\"value\":750000}}],\"vout\":[{\"scriptpubkey_address\":\"bc1qgsx9y62ajme3gg8v9n9jfps2694uy9r6f9unj0\",\"value\":600000},{\"scriptpubkey_address\":\"bc1q6lqf0jehmaadwmdhap98rulflft27z00g0qphn\",\"value\":187144}],\"size\":372,\"weight\":834,\"fee\":12000,\"status\":{\"confirmed\":true,\"block_height\":669442}}", + "ca4a1f991c3f585e4fbbb5b5aeb0766ba3eb46bb1c3ff3714db7a8cadd0e557d": "{\"txid\":\"ca4a1f991c3f585e4fbbb5b5aeb0766ba3eb46bb1c3ff3714db7a8cadd0e557d\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":10237}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":1,\"prevout\":{\"value\":900000}},{\"vout\":2,\"prevout\":{\"value\":8858290}}],\"vout\":[{\"scriptpubkey_address\":\"1DQXP1AXR1qkXficdkfXHHy2JkbtRGFQ1b\",\"value\":10217},{\"scriptpubkey_address\":\"bc1q9jfjvhvr42smvwylrlqcrefcdxagdzf52aquzm\",\"value\":2600000},{\"scriptpubkey_address\":\"bc1qc6qraj5h8qxvluh2um4rvunqn68fltc9kjfrk9\",\"value\":7753730}],\"size\":702,\"weight\":1833,\"fee\":4580,\"status\":{\"confirmed\":true,\"block_height\":667352}}", + "4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189": "{\"txid\":\"4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":23893}},{\"vout\":1,\"prevout\":{\"value\":1440000}},{\"vout\":2,\"prevout\":{\"value\":16390881}}],\"vout\":[{\"scriptpubkey_address\":\"1Kmrzq3WGCQsZw5kroEphuk1KgsEr65yB7\",\"value\":23858},{\"scriptpubkey_address\":\"bc1qyw5qql9m7rkse9mhcun225nrjpwycszsa5dpjg\",\"value\":7015000},{\"scriptpubkey_address\":\"bc1q90y3p6mg0pe3rvvzfeudq4mfxafgpc9rulruff\",\"value\":10774186}],\"size\":554,\"weight\":1559,\"fee\":41730,\"status\":{\"confirmed\":true,\"block_height\":668198}}", + "b9e1a791f4091910caeb70d1a5d56452bc9614c16b5b74281b2485551faeb46e": "{\"txid\":\"b9e1a791f4091910caeb70d1a5d56452bc9614c16b5b74281b2485551faeb46e\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":1432}},{\"vout\":2,\"prevout\":{\"value\":30302311}}],\"vout\":[{\"scriptpubkey_address\":\"16MK64AGvKVF7xu9Xfjh8o7Xo4e1HMhUqq\",\"value\":1422},{\"scriptpubkey_address\":\"bc1qjp535w2zl3cxg02xgdx8yewtvn6twcnj86t73c\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1qa58rfr0wumczmau0qehjwcsdkcgs5dmkg7url5\",\"value\":28698421}],\"size\":405,\"weight\":1287,\"fee\":3900,\"status\":{\"confirmed\":true,\"block_height\":666373}}", + "dc06cd41f4b778553a0a5df4578c62eeb0c9c878b6f1a24c60b619a6749877c7": "{\"txid\":\"dc06cd41f4b778553a0a5df4578c62eeb0c9c878b6f1a24c60b619a6749877c7\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":230000000}}],\"vout\":[{\"scriptpubkey_address\":\"38bZBj5peYS3Husdz7AH3gEUiUbYRD951t\",\"value\":200000},{\"scriptpubkey_address\":\"bc1qaq3v7gjqaeyx7yzkcu59l8f47apkfutr927xa8\",\"value\":30000000},{\"scriptpubkey_address\":\"bc1qx9avgdnkal2jfcfjqdsdu7ly60awl4wcfgk6m0\",\"value\":199793875}],\"size\":255,\"weight\":690,\"fee\":6125,\"status\":{\"confirmed\":true,\"block_height\":668527}}", + "ef1ea38b46402deb7df08c13a6dc379a65542a6940ac9d4ba436641ffd4bcb6e": "{\"txid\":\"ef1ea38b46402deb7df08c13a6dc379a65542a6940ac9d4ba436641ffd4bcb6e\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":61000}},{\"vout\":0,\"prevout\":{\"value\":6415500}}],\"vout\":[{\"scriptpubkey_address\":\"164hNDe95nNsQYVSVbeypn36HqT5uD5AoT\",\"value\":60908},{\"scriptpubkey_address\":\"1MEsN2jLyrcWBMjggSPs88xAnj6D38sQL3\",\"value\":2395500},{\"scriptpubkey_address\":\"1A3pYPW1zQcMpHUnSfPCxYWgCrUW93t2yV\",\"value\":3973352}],\"size\":408,\"weight\":1632,\"fee\":46740,\"status\":{\"confirmed\":true,\"block_height\":640441}}", + "654a7a34321b57be6a553052d1d9e0f1764dd2fab7b64c9422e9953e4d9d127d": "{\"txid\":\"654a7a34321b57be6a553052d1d9e0f1764dd2fab7b64c9422e9953e4d9d127d\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":26000000}}],\"vout\":[{\"scriptpubkey_address\":\"18GzH11T5h2fpvUoBJDub7MgNJVw3FfqQ8\",\"value\":238000},{\"scriptpubkey_address\":\"1JYZ4cba5pjPxXqm5MDGUVvj2k3cZezRaR\",\"value\":3000000},{\"scriptpubkey_address\":\"12DNP86oaEXfEBkow4Kpkw2tNaqoECYhtc\",\"value\":22756800}],\"size\":260,\"weight\":1040,\"fee\":5200,\"status\":{\"confirmed\":true,\"block_height\":554950}}", + "1c8e4934f93b5bbd2823318d5d491698316216f2e4bc0d7cd353f6b16358d80e": "{\"txid\":\"1c8e4934f93b5bbd2823318d5d491698316216f2e4bc0d7cd353f6b16358d80e\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":563209}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":2,\"prevout\":{\"value\":214153}},{\"vout\":2,\"prevout\":{\"value\":116517}},{\"vout\":2,\"prevout\":{\"value\":135306}},{\"vout\":2,\"prevout\":{\"value\":261906}},{\"vout\":2,\"prevout\":{\"value\":598038}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":1,\"prevout\":{\"value\":600932}},{\"vout\":1,\"prevout\":{\"value\":600944}}],\"vout\":[{\"scriptpubkey_address\":\"19qA2BVPoyXDfHKVMovKG7SoxGY7xrBV8c\",\"value\":546},{\"scriptpubkey_address\":\"bc1qwcwu3mx0nmf290y8t0jlukhxujaul0fc4jxe44\",\"value\":4743164},{\"scriptpubkey_address\":\"bc1qduw8nd2sscyezk02xj3a3ks5adh8wmctqaew6g\",\"value\":145131}],\"size\":1746,\"weight\":3417,\"fee\":2164,\"status\":{\"confirmed\":true,\"block_height\":669227}}", + "e125fdbd09ee86c01e16e1f12a31507cfb8703ed1bd5a221461adf33cb3e00d9": "{\"txid\":\"e125fdbd09ee86c01e16e1f12a31507cfb8703ed1bd5a221461adf33cb3e00d9\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":1,\"prevout\":{\"value\":4250960}}],\"vout\":[{\"scriptpubkey_address\":\"1PUXU1MQ82JC3Hx1NN5tZs3BaTAJVg72MC\",\"value\":200000},{\"scriptpubkey_address\":\"1MSkjSzF1dTKR121scX64Brvs4zhExVE8Q\",\"value\":4000000}],\"size\":225,\"weight\":900,\"fee\":50960,\"status\":{\"confirmed\":true,\"block_height\":578733}}", + "051770f8d7f43a9b6ca10fefa6cdf4cb124a81eed26dc8af2e40f57d2589107b": "{\"txid\":\"051770f8d7f43a9b6ca10fefa6cdf4cb124a81eed26dc8af2e40f57d2589107b\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":23985}},{\"vout\":1,\"prevout\":{\"value\":6271500}},{\"vout\":0,\"prevout\":{\"value\":2397000}},{\"vout\":2,\"prevout\":{\"value\":41281331}}],\"vout\":[{\"scriptpubkey_address\":\"16pULNutwpJ5E6EaxopQQDAVaFJXt8B18Z\",\"value\":23893},{\"scriptpubkey_address\":\"bc1q6hkhftt9v5kkcj9wr66ycqy23dqyle3h3wnv50\",\"value\":18365500},{\"scriptpubkey_address\":\"bc1q3ffqm4e4wxdg8jgcw0wlpw4vg9hgwnql3y9zn0\",\"value\":31546169}],\"size\":703,\"weight\":2476,\"fee\":38254,\"status\":{\"confirmed\":true,\"block_height\":667928}}", + "72cabb5c323c923b43c7f6551974f591dcee148778ee34f9131011ea0ca82813": "{\"txid\":\"72cabb5c323c923b43c7f6551974f591dcee148778ee34f9131011ea0ca82813\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":1,\"prevout\":{\"value\":12000000}}],\"vout\":[{\"scriptpubkey_address\":\"3EfRGckBQQuk7cpU7SwatPv8kFD1vALkTU\",\"value\":546},{\"scriptpubkey_address\":\"bc1q6xthjqca0p83mua54e9t0sapxkvc7n3dvwssxc\",\"value\":2600000},{\"scriptpubkey_address\":\"bc1q3uaew9e6uqm6pth8nq7wh3wcwzxwh2q25fggcg\",\"value\":9388079}],\"size\":254,\"weight\":689,\"fee\":11375,\"status\":{\"confirmed\":true,\"block_height\":672972}}", + "17cbd95d8809dc8808a5c209208f59c4a80e09e012e62951668d30d716c44a96": "{\"txid\":\"17cbd95d8809dc8808a5c209208f59c4a80e09e012e62951668d30d716c44a96\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":2810563}}],\"vout\":[{\"scriptpubkey_address\":\"13sxMq8mTw7CTSqgGiMPfwo6ZDsVYrHLmR\",\"value\":546},{\"scriptpubkey_address\":\"bc1qklv4zsl598ujy2ntl5g3wqjxasu2f74egw0tlm\",\"value\":1603262},{\"scriptpubkey_address\":\"bc1qclesyfupj309620thesxmj4vcdscjfykdqz4np\",\"value\":1205124}],\"size\":256,\"weight\":697,\"fee\":1631,\"status\":{\"confirmed\":true,\"block_height\":669340}}", + "cd99836ac4246c3e3980edf95773060481ce52271b74dadeb41e18c42ed21188": "{\"txid\":\"cd99836ac4246c3e3980edf95773060481ce52271b74dadeb41e18c42ed21188\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":200584}},{\"vout\":1,\"prevout\":{\"value\":600000}}],\"vout\":[{\"scriptpubkey_address\":\"bc1q7sd0k2a6p942848y5nsk9cqwdguhd7c04t2t3w\",\"value\":750000},{\"scriptpubkey_address\":\"bc1qrcez45uf02sg6zvk3mqmtlc9vnrvn50jcywlk5\",\"value\":49144}],\"size\":371,\"weight\":833,\"fee\":1440,\"status\":{\"confirmed\":true,\"block_height\":669442}}" +} \ No newline at end of file diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java index 9c3eacd3308..1e01c8a80ab 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -42,6 +42,7 @@ import bisq.core.payment.PaymentAccountUtil; import bisq.core.payment.payload.PaymentMethod; import bisq.core.provider.fee.FeeService; +import bisq.core.provider.mempool.MempoolService; import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.TradeManager; import bisq.core.trade.handlers.TradeResultHandler; @@ -60,14 +61,18 @@ import com.google.inject.Inject; +import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableList; import java.util.Set; +import lombok.Getter; + import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; @@ -86,6 +91,7 @@ class TakeOfferDataModel extends OfferDataModel { private final BsqWalletService bsqWalletService; private final User user; private final FeeService feeService; + private final MempoolService mempoolService; private final FilterManager filterManager; final Preferences preferences; private final TxFeeEstimationService txFeeEstimationService; @@ -113,6 +119,10 @@ class TakeOfferDataModel extends OfferDataModel { private int feeTxVsize = 192; // (175+233+169)/3 private boolean freezeFee; private Coin txFeePerVbyteFromFeeService; + @Getter + protected final IntegerProperty mempoolStatus = new SimpleIntegerProperty(); + @Getter + protected String mempoolStatusText; /////////////////////////////////////////////////////////////////////////////////////////// @@ -127,6 +137,7 @@ class TakeOfferDataModel extends OfferDataModel { BtcWalletService btcWalletService, BsqWalletService bsqWalletService, User user, FeeService feeService, + MempoolService mempoolService, FilterManager filterManager, Preferences preferences, TxFeeEstimationService txFeeEstimationService, @@ -142,6 +153,7 @@ class TakeOfferDataModel extends OfferDataModel { this.bsqWalletService = bsqWalletService; this.user = user; this.feeService = feeService; + this.mempoolService = mempoolService; this.filterManager = filterManager; this.preferences = preferences; this.txFeeEstimationService = txFeeEstimationService; @@ -241,6 +253,15 @@ void initWithData(Offer offer) { } }); + mempoolStatus.setValue(-1); + mempoolService.validateOfferMakerTx(offer.getOfferPayload(), (txValidator -> { + mempoolStatus.setValue(txValidator.isFail() ? 0 : 1); + if (txValidator.isFail()) { + mempoolStatusText = txValidator.toString(); + log.info("Mempool check of OfferFeePaymentTxId returned errors: [{}]", mempoolStatusText); + } + })); + calculateVolume(); calculateTotalToPay(); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java index b7e7c1c60f9..a10e9f08f05 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java @@ -164,6 +164,7 @@ public class TakeOfferView extends ActivatableViewAndModel amountFocusedListener, getShowWalletFundedNotificationListener; + private InfoInputTextField volumeInfoTextField; private AutoTooltipSlideToggleButton tradeFeeInBtcToggle, tradeFeeInBsqToggle; private ChangeListener tradeFeeInBtcToggleListener, tradeFeeInBsqToggleListener, @@ -887,7 +888,7 @@ private void addButtons() { nextButton = tuple.first; nextButton.setMaxWidth(200); nextButton.setDefaultButton(true); - nextButton.setOnAction(e -> showNextStepAfterAmountIsSet()); + nextButton.setOnAction(e -> nextStepCheckMakerTx()); cancelButton1 = tuple.second; cancelButton1.setMaxWidth(200); @@ -898,6 +899,25 @@ private void addButtons() { }); } + private void nextStepCheckMakerTx() { + // the tx validation check has had plenty of time to complete, but if for some reason it has not returned + // we continue anyway since the check is not crucial. + // note, it would be great if there was a real tri-state boolean we could use here, instead of -1, 0, and 1 + int result = model.dataModel.mempoolStatus.get(); + if (result == 0) { + new Popup().warning(Res.get("popup.warning.makerTxInvalid") + model.dataModel.getMempoolStatusText()) + .onClose(() -> { + cancelButton1.fire(); + }) + .show(); + } else { + if (result == -1) { + log.warn("Fee check has not returned a result yet. We optimistically assume all is ok and continue."); + } + showNextStepAfterAmountIsSet(); + } + } + private void showNextStepAfterAmountIsSet() { if (DevEnv.isDaoTradingActivated()) showFeeOption(); @@ -938,7 +958,6 @@ private void showFeeOption() { private void addOfferAvailabilityLabel() { offerAvailabilityBusyAnimation = new BusyAnimation(false); offerAvailabilityLabel = new AutoTooltipLabel(Res.get("takeOffer.fundsBox.isOfferAvailable")); - buttonBox.getChildren().addAll(offerAvailabilityBusyAnimation, offerAvailabilityLabel); } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java index 717ef1ce47a..043db5af5d9 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java @@ -130,6 +130,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im private ChangeListener tradeErrorListener; private ChangeListener offerStateListener; private ChangeListener offerErrorListener; + private ChangeListener getMempoolStatusListener; private ConnectionListener connectionListener; // private Subscription isFeeSufficientSubscription; private Runnable takeOfferSucceededHandler; @@ -462,6 +463,7 @@ private void updateButtonDisableState() { boolean inputDataValid = isBtcInputValid(amount.get()).isValid && dataModel.isMinAmountLessOrEqualAmount() && !dataModel.isAmountLargerThanOfferAmount() + && dataModel.mempoolStatus.get() >= 0 // TODO do we want to block in case response is slow (tor can be slow)? && isOfferAvailable.get() && !dataModel.wouldCreateDustForMaker(); isNextButtonDisabled.set(!inputDataValid); @@ -509,6 +511,13 @@ private void createListeners() { tradeStateListener = (ov, oldValue, newValue) -> applyTradeState(); tradeErrorListener = (ov, oldValue, newValue) -> applyTradeErrorMessage(newValue); offerStateListener = (ov, oldValue, newValue) -> applyOfferState(newValue); + + getMempoolStatusListener = (observable, oldValue, newValue) -> { + if (newValue.longValue() >= 0) { + updateButtonDisableState(); + } + }; + connectionListener = new ConnectionListener() { @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { @@ -558,6 +567,7 @@ private void addListeners() { dataModel.getAmount().addListener(amountAsCoinListener); dataModel.getIsBtcWalletFunded().addListener(isWalletFundedListener); + dataModel.getMempoolStatus().addListener(getMempoolStatusListener); p2PService.getNetworkNode().addConnectionListener(connectionListener); /* isFeeSufficientSubscription = EasyBind.subscribe(dataModel.isFeeFromFundingTxSufficient, newValue -> { updateButtonDisableState(); @@ -570,6 +580,7 @@ private void removeListeners() { // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().removeListener(amountAsCoinListener); + dataModel.getMempoolStatus().removeListener(getMempoolStatusListener); dataModel.getIsBtcWalletFunded().removeListener(isWalletFundedListener); if (offer != null) { diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index 24582555ab2..242395d810c 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -38,6 +38,7 @@ import bisq.core.dao.DaoFacade; import bisq.core.locale.Res; import bisq.core.offer.Offer; +import bisq.core.provider.mempool.MempoolService; import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeList; @@ -84,16 +85,16 @@ import javafx.beans.binding.Bindings; import javafx.beans.value.ChangeListener; +import java.time.Instant; + import java.util.Date; +import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; -import static bisq.desktop.util.FormBuilder.add2ButtonsWithBox; -import static bisq.desktop.util.FormBuilder.addConfirmationLabelLabel; -import static bisq.desktop.util.FormBuilder.addTitledGroupBg; -import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox; +import static bisq.desktop.util.FormBuilder.*; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @@ -104,6 +105,7 @@ public class DisputeSummaryWindow extends Overlay { private final TradeWalletService tradeWalletService; private final BtcWalletService btcWalletService; private final TxFeeEstimationService txFeeEstimationService; + private final MempoolService mempoolService; private final DaoFacade daoFacade; private Dispute dispute; private Optional finalizeDisputeHandlerOptional = Optional.empty(); @@ -120,6 +122,7 @@ public class DisputeSummaryWindow extends Overlay { // Dispute object of other trade peer. The dispute field is the one from which we opened the close dispute window. private Optional peersDisputeOptional; private String role; + private Label delayedPayoutTxStatus; private TextArea summaryNotesTextArea; private ChangeListener customRadioButtonSelectedListener; @@ -141,6 +144,7 @@ public DisputeSummaryWindow(@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormat TradeWalletService tradeWalletService, BtcWalletService btcWalletService, TxFeeEstimationService txFeeEstimationService, + MempoolService mempoolService, DaoFacade daoFacade) { this.formatter = formatter; @@ -149,6 +153,7 @@ public DisputeSummaryWindow(@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormat this.tradeWalletService = tradeWalletService; this.btcWalletService = btcWalletService; this.txFeeEstimationService = txFeeEstimationService; + this.mempoolService = mempoolService; this.daoFacade = daoFacade; type = Type.Confirmation; @@ -161,6 +166,7 @@ public void show(Dispute dispute) { width = 1150; createGridPane(); addContent(); + checkDelayedPayoutTransaction(); display(); if (DevEnv.isDevMode()) { @@ -314,6 +320,26 @@ private void addInfoPane() { " " + formatter.formatCoinWithCode(contract.getOfferPayload().getSellerSecurityDeposit()); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); + + boolean isMediationDispute = getDisputeManager(dispute) instanceof MediationManager; + if (isMediationDispute) { + if (dispute.getTradePeriodEnd().getTime() > 0) { + String status = DisplayUtils.formatDateTime(dispute.getTradePeriodEnd()); + Label tradePeriodEnd = addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.tradePeriodEnd"), status).second; + if (dispute.getTradePeriodEnd().toInstant().isAfter(Instant.now())) { + tradePeriodEnd.getStyleClass().add("version-new"); // highlight field when the trade period is still active + } + } + if (dispute.getExtraDataMap() != null && dispute.getExtraDataMap().size() > 0) { + String extraDataSummary = ""; + for (Map.Entry entry : dispute.getExtraDataMap().entrySet()) { + extraDataSummary += "[" + entry.getKey() + ":" + entry.getValue() + "] "; + } + addConfirmationLabelLabelWithCopyIcon(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.extraInfo"), extraDataSummary); + } + } else { + delayedPayoutTxStatus = addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.delayedPayoutStatus"), "Checking...").second; + } } private void addTradeAmountPayoutControls() { @@ -970,4 +996,25 @@ private void applyTradeAmountRadioButtonStates() { customRadioButton.setSelected(true); } } + + private void checkDelayedPayoutTransaction() { + if (dispute.getDelayedPayoutTxId() == null) + return; + mempoolService.checkTxIsConfirmed(dispute.getDelayedPayoutTxId(), (validator -> { + long confirms = validator.parseJsonValidateTx(); + log.info("Mempool check confirmation status of DelayedPayoutTxId returned: [{}]", confirms); + displayPayoutStatus(confirms); + })); + } + + private void displayPayoutStatus(long nConfirmStatus) { + if (delayedPayoutTxStatus != null) { + String status = Res.get("confidence.unknown"); + if (nConfirmStatus == 0) + status = Res.get("confidence.seen", 1); + else if (nConfirmStatus > 0) + status = Res.get("confidence.confirmed", nConfirmStatus); + delayedPayoutTxStatus.setText(status); + } + } } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java index 5ff3f216eab..017ec644d3f 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java @@ -169,6 +169,8 @@ private void addContent() { Res.get("filterWindow.bannedPrivilegedDevPubKeys")).second; InputTextField autoConfExplorersTF = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.autoConfExplorers")).second; + CheckBox disableMempoolValidationCheckBox = addLabelCheckBox(gridPane, ++rowIndex, + Res.get("filterWindow.disableMempoolValidation")); CheckBox disableApiCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("filterWindow.disableApi")); @@ -196,6 +198,7 @@ private void addContent() { disableAutoConfCheckBox.setSelected(filter.isDisableAutoConf()); disableDaoBelowVersionTF.setText(filter.getDisableDaoBelowVersion()); disableTradeBelowVersionTF.setText(filter.getDisableTradeBelowVersion()); + disableMempoolValidationCheckBox.setSelected(filter.isDisableMempoolValidation()); disableApiCheckBox.setSelected(filter.isDisableApi()); } @@ -231,6 +234,7 @@ private void addContent() { disableAutoConfCheckBox.isSelected(), readAsList(autoConfExplorersTF), new HashSet<>(readAsList(bannedFromNetworkTF)), + disableMempoolValidationCheckBox.isSelected(), disableApiCheckBox.isSelected() ); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 81ef8590f68..7c8e28350e9 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -139,6 +139,7 @@ public interface ChatCallback { private ChangeListener tradeStateListener; private ChangeListener disputeStateListener; private ChangeListener mediationResultStateListener; + private ChangeListener getMempoolStatusListener; /////////////////////////////////////////////////////////////////////////////////////////// @@ -228,6 +229,15 @@ public void initialize() { }; tradesListChangeListener = c -> onListChanged(); + + getMempoolStatusListener = (observable, oldValue, newValue) -> { + // -1 status is unknown + // 0 status is FAIL + // 1 status is PASS + if (newValue.longValue() >= 0) { + log.info("Taker fee validation returned {}", newValue.longValue()); + } + }; } @Override @@ -287,6 +297,7 @@ else if (root.getChildren().size() == 2) list.addListener(tradesListChangeListener); updateNewChatMessagesByTradeMap(); + model.getMempoolStatus().addListener(getMempoolStatusListener); } @Override @@ -298,6 +309,7 @@ protected void deactivate() { removeSelectedSubView(); model.dataModel.list.removeListener(tradesListChangeListener); + model.getMempoolStatus().removeListener(getMempoolStatusListener); if (scene != null) scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index fc08e6bc233..6ca1e8ac5a9 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -19,15 +19,18 @@ import bisq.desktop.common.model.ActivatableWithDataModel; import bisq.desktop.common.model.ViewModel; +import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.GUIUtil; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.btc.wallet.Restrictions; +import bisq.core.locale.Res; import bisq.core.network.MessageState; import bisq.core.offer.Offer; import bisq.core.offer.OfferUtil; import bisq.core.provider.fee.FeeService; +import bisq.core.provider.mempool.MempoolService; import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.trade.TradeUtil; @@ -41,6 +44,7 @@ import bisq.network.p2p.P2PService; import bisq.common.ClockWatcher; +import bisq.common.UserThread; import bisq.common.app.DevEnv; import org.bitcoinj.core.Coin; @@ -52,11 +56,14 @@ import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; +import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import java.util.Date; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import lombok.Getter; @@ -96,6 +103,7 @@ enum SellerState implements State { public final BtcAddressValidator btcAddressValidator; final AccountAgeWitnessService accountAgeWitnessService; public final P2PService p2PService; + private final MempoolService mempoolService; private final ClosedTradableManager closedTradableManager; private final OfferUtil offerUtil; private final TradeUtil tradeUtil; @@ -109,6 +117,8 @@ enum SellerState implements State { private final ObjectProperty messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); private Subscription tradeStateSubscription; private Subscription messageStateSubscription; + @Getter + protected final IntegerProperty mempoolStatus = new SimpleIntegerProperty(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -121,6 +131,7 @@ public PendingTradesViewModel(PendingTradesDataModel dataModel, BsqFormatter bsqFormatter, BtcAddressValidator btcAddressValidator, P2PService p2PService, + MempoolService mempoolService, ClosedTradableManager closedTradableManager, OfferUtil offerUtil, TradeUtil tradeUtil, @@ -133,6 +144,7 @@ public PendingTradesViewModel(PendingTradesDataModel dataModel, this.bsqFormatter = bsqFormatter; this.btcAddressValidator = btcAddressValidator; this.p2PService = p2PService; + this.mempoolService = mempoolService; this.closedTradableManager = closedTradableManager; this.offerUtil = offerUtil; this.tradeUtil = tradeUtil; @@ -191,6 +203,29 @@ private void onMessageStateChanged(MessageState messageState) { messageStateProperty.set(messageState); } + public void checkTakerFeeTx(Trade trade) { + mempoolStatus.setValue(-1); + mempoolService.validateOfferTakerTx(trade, (txValidator -> { + mempoolStatus.setValue(txValidator.isFail() ? 0 : 1); + if (txValidator.isFail()) { + String errorMessage = "Validation of Taker Tx returned: " + txValidator.toString(); + log.warn(errorMessage); + // prompt user to open mediation + if (trade.getDisputeState() == Trade.DisputeState.NO_DISPUTE) { + UserThread.runAfter(() -> { + Popup popup = new Popup(); + popup.headLine(Res.get("portfolio.pending.openSupportTicket.headline")) + .message(Res.get("portfolio.pending.invalidTx", errorMessage)) + .actionButtonText(Res.get("portfolio.pending.openSupportTicket.headline")) + .onAction(dataModel::onOpenSupportTicket) + .closeButtonText(Res.get("shared.cancel")) + .onClose(popup::hide) + .show(); + }, 100, TimeUnit.MILLISECONDS); + } + } + })); + } /////////////////////////////////////////////////////////////////////////////////////////// // Getters diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java index d886d229c85..f6181a12adb 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java @@ -38,6 +38,7 @@ public BuyerStep1View(PendingTradesViewModel model) { protected void onPendingTradesInitialized() { super.onPendingTradesInitialized(); validatePayoutTx(); + validateDepositInputs(); } @@ -89,7 +90,18 @@ private void validatePayoutTx() { // trade manager after initPendingTrades which happens after activate might be called. } catch (TradeDataValidation.ValidationException e) { if (!model.dataModel.tradeManager.isAllowFaultyDelayedTxs()) { - new Popup().warning(Res.get("portfolio.pending.invalidDelayedPayoutTx", e.getMessage())).show(); + new Popup().warning(Res.get("portfolio.pending.invalidTx", e.getMessage())).show(); + } + } + } + + // Verify that deposit tx inputs are matching the trade fee txs outputs. + private void validateDepositInputs() { + try { + TradeDataValidation.validateDepositInputs(trade); + } catch (TradeDataValidation.ValidationException e) { + if (!model.dataModel.tradeManager.isAllowFaultyDelayedTxs()) { + new Popup().warning(Res.get("portfolio.pending.invalidTx", e.getMessage())).show(); } } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java index 1b486be1e4c..21661409d80 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java @@ -195,6 +195,7 @@ public void deactivate() { protected void onPendingTradesInitialized() { super.onPendingTradesInitialized(); validatePayoutTx(); + model.checkTakerFeeTx(trade); } @@ -598,7 +599,7 @@ private void validatePayoutTx() { // trade manager after initPendingTrades which happens after activate might be called. } catch (TradeDataValidation.ValidationException e) { if (!model.dataModel.tradeManager.isAllowFaultyDelayedTxs()) { - new Popup().warning(Res.get("portfolio.pending.invalidDelayedPayoutTx", e.getMessage())).show(); + new Popup().warning(Res.get("portfolio.pending.invalidTx", e.getMessage())).show(); } } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java index a2034ec31d5..defa81fde7e 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java @@ -17,10 +17,12 @@ package bisq.desktop.main.portfolio.pendingtrades.steps.seller; +import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.portfolio.pendingtrades.PendingTradesViewModel; import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import bisq.core.locale.Res; +import bisq.core.trade.TradeDataValidation; public class SellerStep1View extends TradeStepView { @@ -32,6 +34,12 @@ public SellerStep1View(PendingTradesViewModel model) { super(model); } + @Override + protected void onPendingTradesInitialized() { + super.onPendingTradesInitialized(); + validateDepositInputs(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Info /////////////////////////////////////////////////////////////////////////////////////////// @@ -63,6 +71,21 @@ protected String getFirstHalfOverWarnText() { protected String getPeriodOverWarnText() { return Res.get("portfolio.pending.step1.openForDispute"); } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + // Verify that deposit tx inputs are matching the trade fee txs outputs. + private void validateDepositInputs() { + try { + TradeDataValidation.validateDepositInputs(trade); + } catch (TradeDataValidation.ValidationException e) { + if (!model.dataModel.tradeManager.isAllowFaultyDelayedTxs()) { + new Popup().warning(Res.get("portfolio.pending.invalidTx", e.getMessage())).show(); + } + } + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java index e6b8506ce10..372377b308e 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java @@ -62,6 +62,11 @@ public void deactivate() { super.deactivate(); } + @Override + protected void onPendingTradesInitialized() { + super.onPendingTradesInitialized(); + model.checkTakerFeeTx(trade); + } /////////////////////////////////////////////////////////////////////////////////////////// // Info diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 3889328f9ba..a1cfd970fd9 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -689,6 +689,7 @@ message Filter { repeated string banned_auto_conf_explorers = 25; repeated string node_addresses_banned_from_network = 26; bool disable_api = 27; + bool disable_mempool_validation = 28; } // Deprecated