Validate maker/taker fees using mempool lookup

apply @chimp1984 patch.txt code review suggestions
taker tx check moved to trade step 2, after confirmation
solve issue with calculating expected fees for unconfirmed tx
resolve conflict with PR5207 (Disputes UI)
check new offers after 1 block; check Json string not null; warn -> info
remove unused parameter
remove debugging log.warn message
This commit is contained in:
jmacxx 2021-03-03 22:19:23 -06:00
parent 3b1158a908
commit 9fcd65f320
No known key found for this signature in database
GPG key ID: 155297BABFE94A1B
29 changed files with 1420 additions and 13 deletions

View file

@ -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 class Config {
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> 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 class Config {
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),

View file

@ -39,6 +39,7 @@ import bisq.core.payment.AmazonGiftCardAccount;
import bisq.core.payment.RevolutAccount;
import bisq.core.payment.TradeLimits;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.mempool.MempoolService;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.support.dispute.arbitration.ArbitrationManager;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
@ -109,6 +110,7 @@ public class DomainInitialisation {
private final User user;
private final DaoStateSnapshotService daoStateSnapshotService;
private final TriggerPriceService triggerPriceService;
private final MempoolService mempoolService;
@Inject
public DomainInitialisation(ClockWatcher clockWatcher,
@ -145,7 +147,8 @@ public class DomainInitialisation {
MarketAlerts marketAlerts,
User user,
DaoStateSnapshotService daoStateSnapshotService,
TriggerPriceService triggerPriceService) {
TriggerPriceService triggerPriceService,
MempoolService mempoolService) {
this.clockWatcher = clockWatcher;
this.tradeLimits = tradeLimits;
this.arbitrationManager = arbitrationManager;
@ -181,6 +184,7 @@ public class DomainInitialisation {
this.user = user;
this.daoStateSnapshotService = daoStateSnapshotService;
this.triggerPriceService = triggerPriceService;
this.mempoolService = mempoolService;
}
public void initDomainServices(Consumer<String> rejectedTxErrorMessageHandler,
@ -261,6 +265,7 @@ public class DomainInitialisation {
priceAlert.onAllServicesInitialized();
marketAlerts.onAllServicesInitialized();
triggerPriceService.onAllServicesInitialized();
mempoolService.onAllServicesInitialized();
if (revolutAccountsUpdateHandler != null) {
revolutAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream()

View file

@ -98,6 +98,9 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
private final Set<String> 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 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
filter.isDisableAutoConf(),
filter.getBannedAutoConfExplorers(),
filter.getNodeAddressesBannedFromNetwork(),
filter.isDisableMempoolValidation(),
filter.isDisableApi());
}
@ -157,6 +161,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
filter.isDisableAutoConf(),
filter.getBannedAutoConfExplorers(),
filter.getNodeAddressesBannedFromNetwork(),
filter.isDisableMempoolValidation(),
filter.isDisableApi());
}
@ -183,6 +188,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
boolean disableAutoConf,
List<String> bannedAutoConfExplorers,
Set<String> nodeAddressesBannedFromNetwork,
boolean disableMempoolValidation,
boolean disableApi) {
this(bannedOfferIds,
nodeAddressesBannedFromTrading,
@ -210,6 +216,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
disableAutoConf,
bannedAutoConfExplorers,
nodeAddressesBannedFromNetwork,
disableMempoolValidation,
disableApi);
}
@ -245,6 +252,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
boolean disableAutoConf,
List<String> bannedAutoConfExplorers,
Set<String> nodeAddressesBannedFromNetwork,
boolean disableMempoolValidation,
boolean disableApi) {
this.bannedOfferIds = bannedOfferIds;
this.nodeAddressesBannedFromTrading = nodeAddressesBannedFromTrading;
@ -272,6 +280,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
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 final class Filter implements ProtectedStoragePayload, ExpirablePayload {
.setDisableAutoConf(disableAutoConf)
.addAllBannedAutoConfExplorers(bannedAutoConfExplorers)
.addAllNodeAddressesBannedFromNetwork(nodeAddressesBannedFromNetwork)
.setDisableMempoolValidation(disableMempoolValidation)
.setDisableApi(disableApi);
Optional.ofNullable(signatureAsBase64).ifPresent(builder::setSignatureAsBase64);
@ -352,6 +362,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
proto.getDisableAutoConf(),
ProtoUtil.protocolStringListToList(proto.getBannedAutoConfExplorersList()),
ProtoUtil.protocolStringListToSet(proto.getNodeAddressesBannedFromNetworkList()),
proto.getDisableMempoolValidation(),
proto.getDisableApi()
);
}
@ -396,6 +407,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
",\n ownerPubKey=" + ownerPubKey +
",\n disableAutoConf=" + disableAutoConf +
",\n nodeAddressesBannedFromNetwork=" + nodeAddressesBannedFromNetwork +
",\n disableMempoolValidation=" + disableMempoolValidation +
",\n disableApi=" + disableApi +
"\n}";
}

View file

@ -73,6 +73,9 @@ public final class OpenOffer implements Tradable {
// 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);

View file

@ -20,6 +20,7 @@ package bisq.core.offer;
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 @@ import static bisq.common.util.MathUtils.scaleUpByPowerOf10;
public class TriggerPriceService {
private final P2PService p2PService;
private final OpenOfferManager openOfferManager;
private final MempoolService mempoolService;
private final PriceFeedService priceFeedService;
private final Map<String, Set<OpenOffer>> 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 @@ public class TriggerPriceService {
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 -> {
});
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> 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<String> mempoolServiceCallback, String txId) {
mempoolHttpClient.setBaseUrl(getRandomServiceAddress(txBroadcastServices));
ListenableFuture<String> 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<String> txBroadcastServices) {
List<String> list = checkNotNull(txBroadcastServices);
return !list.isEmpty() ? list.get(new Random().nextInt(list.size())) : null;
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> 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<TxValidator> resultHandler) {
validateOfferMakerTx(new TxValidator(daoStateService, offerPayload.getOfferFeePaymentTxId(), Coin.valueOf(offerPayload.getAmount()),
offerPayload.isCurrencyForMakerFeeBtc()), resultHandler);
}
public void validateOfferMakerTx(TxValidator txValidator, Consumer<TxValidator> 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<TxValidator> resultHandler) {
validateOfferTakerTx(new TxValidator(daoStateService, trade.getTakerFeeTxId(), trade.getTradeAmount(),
trade.isCurrencyForTakerFeeBtc()), resultHandler);
}
public void validateOfferTakerTx(TxValidator txValidator, Consumer<TxValidator> 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<TxValidator> 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<String> future = SettableFuture.create();
Futures.addCallback(future, callbackForTxRequest(mempoolRequest, txValidator, resultHandler), MoreExecutors.directExecutor());
mempoolRequest.getTxStatus(future, txId);
}
// ///////////////////////////
private void validateOfferMakerTx(MempoolRequest mempoolRequest,
TxValidator txValidator,
Consumer<TxValidator> resultHandler) {
SettableFuture<String> 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<TxValidator> resultHandler) {
SettableFuture<String> future = SettableFuture.create();
Futures.addCallback(future, callbackForTakerTxValidation(mempoolRequest, txValidator, resultHandler), MoreExecutors.directExecutor());
mempoolRequest.getTxStatus(future, txValidator.getTxId());
}
private FutureCallback<String> callbackForMakerTxValidation(MempoolRequest theRequest,
TxValidator txValidator,
Consumer<TxValidator> resultHandler) {
outstandingRequests++;
FutureCallback<String> 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<String> callbackForTakerTxValidation(MempoolRequest theRequest,
TxValidator txValidator,
Consumer<TxValidator> resultHandler) {
outstandingRequests++;
FutureCallback<String> 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<String> callbackForTxRequest(MempoolRequest theRequest,
TxValidator txValidator,
Consumer<TxValidator> resultHandler) {
outstandingRequests++;
FutureCallback<String> 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<String> getAllBtcFeeReceivers() {
List<String> btcFeeReceivers = new ArrayList<>();
// fee receivers from filter ref: bisq-network/bisq/pull/4294
List<String> 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;
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> 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<String> 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<String> 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<String> 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<JsonArray, JsonArray> 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();
}
}

View file

@ -355,6 +355,27 @@ public class TradeDataValidation {
}
}
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

View file

@ -585,10 +585,9 @@ portfolio.tab.editOpenOffer=Edit offer
portfolio.closedTrades.deviation.help=Percentage price deviation from market
portfolio.pending.invalidDelayedPayoutTx=There is an issue with a missing or invalid transaction.\n\n\
Please do NOT send the fiat or altcoin payment. Contact Bisq \
developers on Keybase [HYPERLINK:https://keybase.io/team/bisq] or on the \
forum [HYPERLINK:https://bisq.community] for further assistance.\n\n\
portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\n\
Please do NOT send the fiat or altcoin payment.\n\n\
Open a support ticket to get assistance from a Mediator.\n\n\
Error message: {0}
portfolio.pending.step1.waitForConf=Wait for blockchain confirmation
@ -2534,6 +2533,9 @@ disputeSummaryWindow.payoutAmount.buyer=Buyer's payout amount
disputeSummaryWindow.payoutAmount.seller=Seller's payout amount
disputeSummaryWindow.payoutAmount.invert=Use loser as publisher
disputeSummaryWindow.reason=Reason of dispute
disputeSummaryWindow.tradePeriodEnd=Trade period end
disputeSummaryWindow.extraInfo=Extra information
disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status
# dynamic values are not recognized by IntelliJ
# suppress inspection "UnusedProperty"
@ -2642,6 +2644,7 @@ filterWindow.add=Add filter
filterWindow.remove=Remove filter
filterWindow.btcFeeReceiverAddresses=BTC fee receiver addresses
filterWindow.disableApi=Disable API
filterWindow.disableMempoolValidation=Disable Mempool Validation
offerDetailsWindow.minBtcAmount=Min. BTC amount
offerDetailsWindow.min=(min. {0})
@ -2850,6 +2853,10 @@ popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\n\
Trade ID: {2}.\n\n\
Please open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"."
popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n
takeOffer.cancelButton=Cancel take-offer
takeOffer.warningButton=Ignore and continue anyway
# suppress inspection "UnusedProperty"
popup.warning.nodeBanned=One of the {0} nodes got banned.
# suppress inspection "UnusedProperty"

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> 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<String, String> goodOffers = loadJsonTestData("offerTestData.json");
Map<String, String> 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<String, String> badOffers = loadJsonTestData("badOfferTestData.json");
Map<String, String> 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<String, String> offers, Map<String, String> mempoolData, boolean expectedResult) {
Set<String> 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<String, String> loadJsonTestData(String fileName) {
String json = "";
try {
json = IOUtils.toString(this.getClass().getResourceAsStream(fileName), "UTF-8");
} catch (IOException e) {
log.error(e.toString());
}
Map<String, String> 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<Coin> mockGetMakerFeeBsq = invocation -> {
return mockedGetMakerFeeBsq(invocation.getArgument(1));
};
Answer<Coin> mockGetTakerFeeBsq = invocation -> {
return mockedGetTakerFeeBsq(invocation.getArgument(1));
};
Answer<Coin> mockGetMakerFeeBtc = invocation -> {
return mockedGetMakerFeeBtc(invocation.getArgument(1));
};
Answer<Coin> 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<Long, String> 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<Long, String> 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<Long, String> 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<Long, String> 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<Long, String> feeMap = new LinkedHashMap<>();
feeMap.put(623227L, "0.0010");
feeMap.put(585787L, "0.0020");
for (Map.Entry<Long, String> 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<Long, String> feeMap = new LinkedHashMap<>();
feeMap.put(623227L, "0.0070");
feeMap.put(585787L, "0.0060");
for (Map.Entry<Long, String> entry : feeMap.entrySet()) {
if (blockHeight >= entry.getKey()) {
return ParsingUtils.parseToCoin(entry.getValue(), bsqFormatter);
}
}
return ParsingUtils.parseToCoin("0.003", bsqFormatter);
}
}

View file

@ -68,6 +68,7 @@ public class UserPayloadModelVOTest {
false,
Lists.newArrayList(),
new HashSet<>(),
false,
false));
vo.setRegisteredArbitrator(ArbitratorTest.getArbitratorMock());

View file

@ -126,6 +126,7 @@ public class FeeReceiverSelectorTest {
false,
Lists.newArrayList(),
new HashSet<>(),
false,
false);
}
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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}}"
}

View file

@ -42,6 +42,7 @@ import bisq.core.payment.PaymentAccount;
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 org.bitcoinj.wallet.Wallet;
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 @@ class TakeOfferDataModel extends OfferDataModel {
}
});
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();

View file

@ -164,6 +164,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
private boolean offerDetailsWindowDisplayed, clearXchangeWarningDisplayed, fasterPaymentsWarningDisplayed;
private SimpleBooleanProperty errorPopupDisplayed;
private ChangeListener<Boolean> amountFocusedListener, getShowWalletFundedNotificationListener;
private InfoInputTextField volumeInfoTextField;
private AutoTooltipSlideToggleButton tradeFeeInBtcToggle, tradeFeeInBsqToggle;
private ChangeListener<Boolean> tradeFeeInBtcToggleListener, tradeFeeInBsqToggleListener,
@ -887,7 +888,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
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 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
});
}
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 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
private void addOfferAvailabilityLabel() {
offerAvailabilityBusyAnimation = new BusyAnimation(false);
offerAvailabilityLabel = new AutoTooltipLabel(Res.get("takeOffer.fundsBox.isOfferAvailable"));
buttonBox.getChildren().addAll(offerAvailabilityBusyAnimation, offerAvailabilityLabel);
}

View file

@ -130,6 +130,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
private ChangeListener<String> tradeErrorListener;
private ChangeListener<Offer.State> offerStateListener;
private ChangeListener<String> offerErrorListener;
private ChangeListener<Number> getMempoolStatusListener;
private ConnectionListener connectionListener;
// private Subscription isFeeSufficientSubscription;
private Runnable takeOfferSucceededHandler;
@ -462,6 +463,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
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 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
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 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
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 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
// 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) {

View file

@ -38,6 +38,7 @@ import bisq.core.btc.wallet.TxBroadcaster;
import bisq.core.dao.DaoFacade;
import bisq.core.locale.Res;
import bisq.core.offer.Offer;
import bisq.core.provider.mempool.MempoolService;
import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeList;
@ -84,16 +85,16 @@ import javafx.geometry.Insets;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import java.time.Instant;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import static bisq.desktop.util.FormBuilder.add2ButtonsWithBox;
import static bisq.desktop.util.FormBuilder.addConfirmationLabelLabel;
import static bisq.desktop.util.FormBuilder.addTitledGroupBg;
import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox;
import static bisq.desktop.util.FormBuilder.*;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
@ -104,6 +105,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
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<Runnable> finalizeDisputeHandlerOptional = Optional.empty();
@ -120,6 +122,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
// Dispute object of other trade peer. The dispute field is the one from which we opened the close dispute window.
private Optional<Dispute> peersDisputeOptional;
private String role;
private Label delayedPayoutTxStatus;
private TextArea summaryNotesTextArea;
private ChangeListener<Boolean> customRadioButtonSelectedListener;
@ -141,6 +144,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
TradeWalletService tradeWalletService,
BtcWalletService btcWalletService,
TxFeeEstimationService txFeeEstimationService,
MempoolService mempoolService,
DaoFacade daoFacade) {
this.formatter = formatter;
@ -149,6 +153,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
this.tradeWalletService = tradeWalletService;
this.btcWalletService = btcWalletService;
this.txFeeEstimationService = txFeeEstimationService;
this.mempoolService = mempoolService;
this.daoFacade = daoFacade;
type = Type.Confirmation;
@ -161,6 +166,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
width = 1150;
createGridPane();
addContent();
checkDelayedPayoutTransaction();
display();
if (DevEnv.isDevMode()) {
@ -314,6 +320,26 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
" " +
formatter.formatCoinWithCode(contract.getOfferPayload().getSellerSecurityDeposit());
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit);
boolean isMediationDispute = getDisputeManager(dispute) instanceof MediationManager;
if (isMediationDispute) {
if (dispute.getTradePeriodEnd().getTime() > 0) {
String status = DisplayUtils.formatDateTime(dispute.getTradePeriodEnd());
Label tradePeriodEnd = addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.tradePeriodEnd"), status).second;
if (dispute.getTradePeriodEnd().toInstant().isAfter(Instant.now())) {
tradePeriodEnd.getStyleClass().add("version-new"); // highlight field when the trade period is still active
}
}
if (dispute.getExtraDataMap() != null && dispute.getExtraDataMap().size() > 0) {
String extraDataSummary = "";
for (Map.Entry<String, String> entry : dispute.getExtraDataMap().entrySet()) {
extraDataSummary += "[" + entry.getKey() + ":" + entry.getValue() + "] ";
}
addConfirmationLabelLabelWithCopyIcon(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.extraInfo"), extraDataSummary);
}
} else {
delayedPayoutTxStatus = addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.delayedPayoutStatus"), "Checking...").second;
}
}
private void addTradeAmountPayoutControls() {
@ -970,4 +996,25 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
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);
}
}
}

View file

@ -169,6 +169,8 @@ public class FilterWindow extends Overlay<FilterWindow> {
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 @@ public class FilterWindow extends Overlay<FilterWindow> {
disableAutoConfCheckBox.setSelected(filter.isDisableAutoConf());
disableDaoBelowVersionTF.setText(filter.getDisableDaoBelowVersion());
disableTradeBelowVersionTF.setText(filter.getDisableTradeBelowVersion());
disableMempoolValidationCheckBox.setSelected(filter.isDisableMempoolValidation());
disableApiCheckBox.setSelected(filter.isDisableApi());
}
@ -231,6 +234,7 @@ public class FilterWindow extends Overlay<FilterWindow> {
disableAutoConfCheckBox.isSelected(),
readAsList(autoConfExplorersTF),
new HashSet<>(readAsList(bannedFromNetworkTF)),
disableMempoolValidationCheckBox.isSelected(),
disableApiCheckBox.isSelected()
);

View file

@ -139,6 +139,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
private ChangeListener<Trade.State> tradeStateListener;
private ChangeListener<Trade.DisputeState> disputeStateListener;
private ChangeListener<MediationResultState> mediationResultStateListener;
private ChangeListener<Number> getMempoolStatusListener;
///////////////////////////////////////////////////////////////////////////////////////////
@ -228,6 +229,15 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
};
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 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
list.addListener(tradesListChangeListener);
updateNewChatMessagesByTradeMap();
model.getMempoolStatus().addListener(getMempoolStatusListener);
}
@Override
@ -298,6 +309,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
removeSelectedSubView();
model.dataModel.list.removeListener(tradesListChangeListener);
model.getMempoolStatus().removeListener(getMempoolStatusListener);
if (scene != null)
scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler);

View file

@ -19,15 +19,18 @@ package bisq.desktop.main.portfolio.pendingtrades;
import bisq.desktop.common.model.ActivatableWithDataModel;
import bisq.desktop.common.model.ViewModel;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.btc.wallet.Restrictions;
import bisq.core.locale.Res;
import bisq.core.network.MessageState;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferUtil;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.mempool.MempoolService;
import bisq.core.trade.Contract;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeUtil;
@ -41,6 +44,7 @@ import bisq.core.util.validation.BtcAddressValidator;
import bisq.network.p2p.P2PService;
import bisq.common.ClockWatcher;
import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import org.bitcoinj.core.Coin;
@ -52,11 +56,14 @@ import javax.inject.Named;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.Getter;
@ -96,6 +103,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
public final BtcAddressValidator btcAddressValidator;
final AccountAgeWitnessService accountAgeWitnessService;
public final P2PService p2PService;
private final MempoolService mempoolService;
private final ClosedTradableManager closedTradableManager;
private final OfferUtil offerUtil;
private final TradeUtil tradeUtil;
@ -109,6 +117,8 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
private final ObjectProperty<MessageState> messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
private Subscription tradeStateSubscription;
private Subscription messageStateSubscription;
@Getter
protected final IntegerProperty mempoolStatus = new SimpleIntegerProperty();
///////////////////////////////////////////////////////////////////////////////////////////
@ -121,6 +131,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
BsqFormatter bsqFormatter,
BtcAddressValidator btcAddressValidator,
P2PService p2PService,
MempoolService mempoolService,
ClosedTradableManager closedTradableManager,
OfferUtil offerUtil,
TradeUtil tradeUtil,
@ -133,6 +144,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
this.bsqFormatter = bsqFormatter;
this.btcAddressValidator = btcAddressValidator;
this.p2PService = p2PService;
this.mempoolService = mempoolService;
this.closedTradableManager = closedTradableManager;
this.offerUtil = offerUtil;
this.tradeUtil = tradeUtil;
@ -191,6 +203,29 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
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

View file

@ -38,6 +38,7 @@ public class BuyerStep1View extends TradeStepView {
protected void onPendingTradesInitialized() {
super.onPendingTradesInitialized();
validatePayoutTx();
validateDepositInputs();
}
@ -89,7 +90,18 @@ public class BuyerStep1View extends TradeStepView {
// 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();
}
}
}

View file

@ -195,6 +195,7 @@ public class BuyerStep2View extends TradeStepView {
protected void onPendingTradesInitialized() {
super.onPendingTradesInitialized();
validatePayoutTx();
model.checkTakerFeeTx(trade);
}
@ -598,7 +599,7 @@ public class BuyerStep2View extends TradeStepView {
// 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();
}
}
}

View file

@ -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 class SellerStep1View extends TradeStepView {
super(model);
}
@Override
protected void onPendingTradesInitialized() {
super.onPendingTradesInitialized();
validateDepositInputs();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Info
///////////////////////////////////////////////////////////////////////////////////////////
@ -63,6 +71,21 @@ public class SellerStep1View extends TradeStepView {
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();
}
}
}
}

View file

@ -62,6 +62,11 @@ public class SellerStep2View extends TradeStepView {
super.deactivate();
}
@Override
protected void onPendingTradesInitialized() {
super.onPendingTradesInitialized();
model.checkTakerFeeTx(trade);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Info

View file

@ -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