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 d9b97421fd8..b0dbd16b774 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; @@ -110,6 +111,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, @@ -146,7 +148,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; @@ -182,6 +185,7 @@ public DomainInitialisation(ClockWatcher clockWatcher, this.user = user; this.daoStateSnapshotService = daoStateSnapshotService; this.triggerPriceService = triggerPriceService; + this.mempoolService = mempoolService; } public void initDomainServices(Consumer rejectedTxErrorMessageHandler, @@ -264,6 +268,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 2a5a81467a9..3160125d8a0 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -586,10 +586,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 @@ -2650,6 +2649,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}) @@ -2869,6 +2869,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 4680549b6e4..40c1f1e606a 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 d2298d39bb7..db15f1832f8 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; @@ -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()) { @@ -315,6 +321,8 @@ private void addInfoPane() { } addConfirmationLabelLabelWithCopyIcon(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.extraInfo"), extraDataSummary); } + } else { + delayedPayoutTxStatus = addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.delayedPayoutStatus"), "Checking...").second; } } @@ -972,4 +980,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 2864435029b..a8c18935576 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 884db025ebb..0eb82248c6f 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 @@ -20,15 +20,18 @@ import bisq.desktop.Navigation; 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; @@ -42,6 +45,7 @@ import bisq.network.p2p.P2PService; import bisq.common.ClockWatcher; +import bisq.common.UserThread; import bisq.common.app.DevEnv; import org.bitcoinj.core.Coin; @@ -53,11 +57,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; @@ -97,6 +104,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; @@ -112,6 +120,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(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -124,6 +134,7 @@ public PendingTradesViewModel(PendingTradesDataModel dataModel, BsqFormatter bsqFormatter, BtcAddressValidator btcAddressValidator, P2PService p2PService, + MempoolService mempoolService, ClosedTradableManager closedTradableManager, OfferUtil offerUtil, TradeUtil tradeUtil, @@ -137,6 +148,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; @@ -196,6 +208,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 18ca09214a2..fd6e456ba2a 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 @@ -203,6 +203,7 @@ public void deactivate() { protected void onPendingTradesInitialized() { super.onPendingTradesInitialized(); validatePayoutTx(); + model.checkTakerFeeTx(trade); } @@ -618,7 +619,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 db08d57f4b8..8ad16257d65 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