randomize trade amount +-10%, price +-1%, date within 48 hours (fork) (#1815)

This commit is contained in:
woodser 2025-07-21 09:56:04 -04:00 committed by GitHub
parent 79dbe34359
commit 51d40d73a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 104 additions and 49 deletions

View file

@ -107,12 +107,11 @@ public class Version {
// The version no. of the current protocol. The offer holds that version. // The version no. of the current protocol. The offer holds that version.
// A taker will check the version of the offers to see if his version is compatible. // A taker will check the version of the offers to see if his version is compatible.
// For the switch to version 2, offers created with the old version will become invalid and have to be canceled.
// For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening
// the Haveno app. // the Haveno app.
// Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1 // Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
// Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2 // Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2
public static final int TRADE_PROTOCOL_VERSION = 2; // Version = 1.2.0 -> TRADE_PROTOCOL_VERSION = 3
public static final int TRADE_PROTOCOL_VERSION = 3;
private static String p2pMessageVersion; private static String p2pMessageVersion;
public static String getP2PMessageVersion() { public static String getP2PMessageVersion() {

View file

@ -74,7 +74,9 @@ class CorePriceService {
public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException { public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException {
var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode)); var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode));
if (marketPrice == null) { if (marketPrice == null) {
throw new IllegalArgumentException("Currency not found: " + currencyCode); // message sent to client throw new IllegalArgumentException("Currency not found: " + currencyCode); // TODO: do not use IllegalArgumentException as message sent to client, return undefined?
} else if (!marketPrice.isExternallyProvidedPrice()) {
throw new IllegalArgumentException("Price is not available externally: " + currencyCode); // TODO: return more complex Price type including price double and isExternal boolean
} }
return mapPriceFeedServicePrice(marketPrice.getPrice(), marketPrice.getCurrencyCode()); return mapPriceFeedServicePrice(marketPrice.getPrice(), marketPrice.getCurrencyCode());
} }

View file

@ -151,7 +151,7 @@ public class CreateOfferService {
// verify price // verify price
boolean useMarketBasedPriceValue = fixedPrice == null && boolean useMarketBasedPriceValue = fixedPrice == null &&
useMarketBasedPrice && useMarketBasedPrice &&
isMarketPriceAvailable(currencyCode) && isExternalPriceAvailable(currencyCode) &&
!PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId()); !PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId());
if (fixedPrice == null && !useMarketBasedPriceValue) { if (fixedPrice == null && !useMarketBasedPriceValue) {
throw new IllegalArgumentException("Must provide fixed price"); throw new IllegalArgumentException("Must provide fixed price");
@ -338,7 +338,7 @@ public class CreateOfferService {
// Private // Private
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private boolean isMarketPriceAvailable(String currencyCode) { private boolean isExternalPriceAvailable(String currencyCode) {
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
return marketPrice != null && marketPrice.isExternallyProvidedPrice(); return marketPrice != null && marketPrice.isExternallyProvidedPrice();
} }

View file

@ -40,6 +40,7 @@ import haveno.core.support.dispute.arbitration.ArbitrationManager;
import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator;
import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentReceivedMessage;
import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.trade.messages.PaymentSentMessage;
import haveno.core.trade.statistics.TradeStatisticsManager;
import haveno.core.user.Preferences; import haveno.core.user.Preferences;
import haveno.core.util.JsonUtil; import haveno.core.util.JsonUtil;
import haveno.core.xmr.wallet.XmrWalletService; import haveno.core.xmr.wallet.XmrWalletService;
@ -134,6 +135,7 @@ public class HavenoUtils {
public static OpenOfferManager openOfferManager; public static OpenOfferManager openOfferManager;
public static CoreNotificationService notificationService; public static CoreNotificationService notificationService;
public static CorePaymentAccountsService corePaymentAccountService; public static CorePaymentAccountsService corePaymentAccountService;
public static TradeStatisticsManager tradeStatisticsManager;
public static Preferences preferences; public static Preferences preferences;
public static boolean isSeedNode() { public static boolean isSeedNode() {

View file

@ -65,7 +65,6 @@ import haveno.core.trade.protocol.ProcessModelServiceProvider;
import haveno.core.trade.protocol.TradeListener; import haveno.core.trade.protocol.TradeListener;
import haveno.core.trade.protocol.TradePeer; import haveno.core.trade.protocol.TradePeer;
import haveno.core.trade.protocol.TradeProtocol; import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.trade.statistics.TradeStatistics3;
import haveno.core.util.VolumeUtil; import haveno.core.util.VolumeUtil;
import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletBase; import haveno.core.xmr.wallet.XmrWalletBase;
@ -124,6 +123,7 @@ import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
@ -2446,12 +2446,27 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
} }
public void maybePublishTradeStatistics() { public void maybePublishTradeStatistics() {
if (shouldPublishTradeStatistics()) doPublishTradeStatistics(); if (shouldPublishTradeStatistics()) {
// publish after random delay within 24 hours
UserThread.runAfterRandomDelay(() -> {
if (!isShutDownStarted) doPublishTradeStatistics();
}, 0, 24, TimeUnit.HOURS);
}
} }
public boolean shouldPublishTradeStatistics() { public boolean shouldPublishTradeStatistics() {
if (!isSeller()) return false;
return tradeAmountTransferred(); // do not publish if funds not transferred
if (!tradeAmountTransferred()) return false;
// only seller or arbitrator publish trade stats
if (!isSeller() && !isArbitrator()) return false;
// prior to v3 protocol, only seller publishes trade stats
if (getOffer().getOfferPayload().getProtocolVersion() < 3 && !isSeller()) return false;
return true;
} }
@ -2466,13 +2481,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
private void doPublishTradeStatistics() { private void doPublishTradeStatistics() {
String referralId = processModel.getReferralIdService().getOptionalReferralId().orElse(null); String referralId = processModel.getReferralIdService().getOptionalReferralId().orElse(null);
boolean isTorNetworkNode = getProcessModel().getP2PService().getNetworkNode() instanceof TorNetworkNode; boolean isTorNetworkNode = getProcessModel().getP2PService().getNetworkNode() instanceof TorNetworkNode;
TradeStatistics3 tradeStatistics = TradeStatistics3.from(this, referralId, isTorNetworkNode, true); HavenoUtils.tradeStatisticsManager.maybePublishTradeStatistics(this, referralId, isTorNetworkNode);
if (tradeStatistics.isValid()) {
log.info("Publishing trade statistics for {} {}", getClass().getSimpleName(), getId());
processModel.getP2PService().addPersistableNetworkPayload(tradeStatistics, true);
} else {
log.warn("Trade statistics are invalid for {} {}. We do not publish: {}", getClass().getSimpleName(), getId(), tradeStatistics);
}
} }
// lazy initialization // lazy initialization

View file

@ -523,7 +523,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
nonFailedTrades.addAll(tradableList.getList()); nonFailedTrades.addAll(tradableList.getList());
String referralId = referralIdService.getOptionalReferralId().orElse(null); String referralId = referralIdService.getOptionalReferralId().orElse(null);
boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode; boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode;
tradeStatisticsManager.maybeRepublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode); tradeStatisticsManager.maybePublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode);
}).start(); }).start();
// allow execution to start // allow execution to start

View file

@ -71,7 +71,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask {
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
Version.getP2PMessageVersion(), Version.getP2PMessageVersion(),
request.getAccountAgeWitnessSignatureOfOfferId(), request.getAccountAgeWitnessSignatureOfOfferId(),
new Date().getTime(), request.getCurrentDate(),
trade.getMaker().getNodeAddress(), trade.getMaker().getNodeAddress(),
trade.getTaker().getNodeAddress(), trade.getTaker().getNodeAddress(),
trade.getArbitrator().getNodeAddress(), trade.getArbitrator().getNodeAddress(),

View file

@ -131,7 +131,7 @@ public class MakerSendInitTradeRequestToArbitrator extends TradeTask {
takerRequest.getUid(), takerRequest.getUid(),
Version.getP2PMessageVersion(), Version.getP2PMessageVersion(),
null, null,
takerRequest.getCurrentDate(), trade.getTakeOfferDate().getTime(), // maker's date is used as shared timestamp
trade.getMaker().getNodeAddress(), trade.getMaker().getNodeAddress(),
trade.getTaker().getNodeAddress(), trade.getTaker().getNodeAddress(),
trade.getArbitrator().getNodeAddress(), trade.getArbitrator().getNodeAddress(),

View file

@ -98,6 +98,7 @@ public class ProcessInitTradeRequest extends TradeTask {
sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
if (sender == trade.getMaker()) { if (sender == trade.getMaker()) {
trade.getTaker().setPubKeyRing(request.getTakerPubKeyRing()); trade.getTaker().setPubKeyRing(request.getTakerPubKeyRing());
trade.setTakeOfferDate(request.getCurrentDate());
// check trade price // check trade price
try { try {
@ -116,6 +117,7 @@ public class ProcessInitTradeRequest extends TradeTask {
if (!trade.getTaker().getPubKeyRing().equals(request.getTakerPubKeyRing())) throw new RuntimeException("Taker's pub key ring does not match request's pub key ring"); if (!trade.getTaker().getPubKeyRing().equals(request.getTakerPubKeyRing())) throw new RuntimeException("Taker's pub key ring does not match request's pub key ring");
if (request.getTradeAmount() != trade.getAmount().longValueExact()) throw new RuntimeException("Trade amount does not match request's trade amount"); if (request.getTradeAmount() != trade.getAmount().longValueExact()) throw new RuntimeException("Trade amount does not match request's trade amount");
if (request.getTradePrice() != trade.getPrice().getValue()) throw new RuntimeException("Trade price does not match request's trade price"); if (request.getTradePrice() != trade.getPrice().getValue()) throw new RuntimeException("Trade price does not match request's trade price");
if (request.getCurrentDate() != trade.getTakeOfferDate().getTime()) throw new RuntimeException("Trade's take offer date does not match request's current date");
} }
// handle invalid sender // handle invalid sender
@ -134,6 +136,7 @@ public class ProcessInitTradeRequest extends TradeTask {
trade.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); trade.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
if (sender != trade.getArbitrator()) throw new RuntimeException("InitTradeRequest to taker is expected from arbitrator"); if (sender != trade.getArbitrator()) throw new RuntimeException("InitTradeRequest to taker is expected from arbitrator");
trade.setTakeOfferDate(request.getCurrentDate());
} }
// handle invalid trade type // handle invalid trade type

View file

@ -69,13 +69,26 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
@JsonExclude @JsonExclude
private transient static final ZoneId ZONE_ID = ZoneId.systemDefault(); private transient static final ZoneId ZONE_ID = ZoneId.systemDefault();
private static final double FUZZ_AMOUNT_PCT = 0.05;
private static final int FUZZ_DATE_HOURS = 24;
public static TradeStatistics3 from(Trade trade, public static TradeStatistics3 fromV0(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) {
@Nullable String referralId, return from(trade, referralId, isTorNetworkNode, 0.0, 0, 0);
boolean isTorNetworkNode, }
boolean isFuzzed) {
public static TradeStatistics3 fromV1(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) {
return from(trade, referralId, isTorNetworkNode, 0.05, 24, 0);
}
public static TradeStatistics3 fromV2(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) {
return from(trade, referralId, isTorNetworkNode, 0.10, 48, .01);
}
// randomize completed trade info #1099
private static TradeStatistics3 from(Trade trade,
@Nullable String referralId,
boolean isTorNetworkNode,
double fuzzAmountPct,
int fuzzDateHours,
double fuzzPricePct) {
Map<String, String> extraDataMap = new HashMap<>(); Map<String, String> extraDataMap = new HashMap<>();
if (referralId != null) { if (referralId != null) {
extraDataMap.put(OfferPayload.REFERRAL_ID, referralId); extraDataMap.put(OfferPayload.REFERRAL_ID, referralId);
@ -92,27 +105,39 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
Offer offer = checkNotNull(trade.getOffer()); Offer offer = checkNotNull(trade.getOffer());
return new TradeStatistics3(offer.getCurrencyCode(), return new TradeStatistics3(offer.getCurrencyCode(),
trade.getPrice().getValue(), fuzzTradePriceReproducibly(trade, fuzzPricePct),
isFuzzed ? fuzzTradeAmountReproducibly(trade) : trade.getAmount().longValueExact(), fuzzTradeAmountReproducibly(trade, fuzzAmountPct),
offer.getPaymentMethod().getId(), offer.getPaymentMethod().getId(),
isFuzzed ? fuzzTradeDateReproducibly(trade) : trade.getTakeOfferDate().getTime(), fuzzTradeDateReproducibly(trade, fuzzDateHours),
truncatedArbitratorNodeAddress, truncatedArbitratorNodeAddress,
extraDataMap); extraDataMap);
} }
private static long fuzzTradeAmountReproducibly(Trade trade) { // randomize completed trade info #1099 private static long fuzzTradePriceReproducibly(Trade trade, double fuzzPricePct) {
if (fuzzPricePct == 0.0) return trade.getPrice().getValue();
long originalTimestamp = trade.getTakeOfferDate().getTime(); long originalTimestamp = trade.getTakeOfferDate().getTime();
Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp
long exactPrice = trade.getPrice().getValue();
long adjustedPrice = (long) random.nextDouble(exactPrice * (1.0 - fuzzPricePct), exactPrice * (1.0 + fuzzPricePct));
log.debug("trade {} fuzzed trade price for tradeStatistics is {}", trade.getShortId(), adjustedPrice);
return adjustedPrice;
}
private static long fuzzTradeAmountReproducibly(Trade trade, double fuzzAmountPct) {
if (fuzzAmountPct == 0.0) return trade.getAmount().longValueExact();
long originalTimestamp = trade.getTakeOfferDate().getTime();
Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp
long exactAmount = trade.getAmount().longValueExact(); long exactAmount = trade.getAmount().longValueExact();
Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp long adjustedAmount = (long) random.nextDouble(exactAmount * (1.0 - fuzzAmountPct), exactAmount * (1.0 + fuzzAmountPct));
long adjustedAmount = (long) random.nextDouble(exactAmount * (1.0 - FUZZ_AMOUNT_PCT), exactAmount * (1 + FUZZ_AMOUNT_PCT));
log.debug("trade {} fuzzed trade amount for tradeStatistics is {}", trade.getShortId(), adjustedAmount); log.debug("trade {} fuzzed trade amount for tradeStatistics is {}", trade.getShortId(), adjustedAmount);
return adjustedAmount; return adjustedAmount;
} }
private static long fuzzTradeDateReproducibly(Trade trade) { // randomize completed trade info #1099 private static long fuzzTradeDateReproducibly(Trade trade, int fuzzDateHours) {
if (fuzzDateHours == 0) return trade.getTakeOfferDate().getTime();
long originalTimestamp = trade.getTakeOfferDate().getTime(); long originalTimestamp = trade.getTakeOfferDate().getTime();
Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp
long adjustedTimestamp = random.nextLong(originalTimestamp - TimeUnit.HOURS.toMillis(FUZZ_DATE_HOURS), originalTimestamp); long adjustedTimestamp = random.nextLong(originalTimestamp - TimeUnit.HOURS.toMillis(fuzzDateHours), originalTimestamp);
log.debug("trade {} fuzzed trade datestamp for tradeStatistics is {}", trade.getShortId(), new Date(adjustedTimestamp)); log.debug("trade {} fuzzed trade datestamp for tradeStatistics is {}", trade.getShortId(), new Date(adjustedTimestamp));
return adjustedTimestamp; return adjustedTimestamp;
} }

View file

@ -26,6 +26,7 @@ import haveno.core.locale.CurrencyTuple;
import haveno.core.locale.CurrencyUtil; import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.Res; import haveno.core.locale.Res;
import haveno.core.provider.price.PriceFeedService; import haveno.core.provider.price.PriceFeedService;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.util.JsonUtil; import haveno.core.util.JsonUtil;
import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PService;
@ -68,8 +69,8 @@ public class TradeStatisticsManager {
this.storageDir = storageDir; this.storageDir = storageDir;
this.dumpStatistics = dumpStatistics; this.dumpStatistics = dumpStatistics;
appendOnlyDataStoreService.addService(tradeStatistics3StorageService); appendOnlyDataStoreService.addService(tradeStatistics3StorageService);
HavenoUtils.tradeStatisticsManager = this;
} }
public void shutDown() { public void shutDown() {
@ -208,7 +209,13 @@ public class TradeStatisticsManager {
jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(array), "trade_statistics"); jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(array), "trade_statistics");
} }
public void maybeRepublishTradeStatistics(Set<Trade> trades, public void maybePublishTradeStatistics(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) {
Set<Trade> trades = new HashSet<>();
trades.add(trade);
maybePublishTradeStatistics(trades, referralId, isTorNetworkNode);
}
public void maybePublishTradeStatistics(Set<Trade> trades,
@Nullable String referralId, @Nullable String referralId,
boolean isTorNetworkNode) { boolean isTorNetworkNode) {
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
@ -219,38 +226,46 @@ public class TradeStatisticsManager {
return; return;
} }
TradeStatistics3 tradeStatistics3 = null; TradeStatistics3 tradeStatistics3V0 = null;
try { try {
tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode, false); tradeStatistics3V0 = TradeStatistics3.fromV0(trade, referralId, isTorNetworkNode);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage()); log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage());
return; return;
} }
TradeStatistics3 tradeStatistics3Fuzzed = null; TradeStatistics3 tradeStatistics3V1 = null;
try { try {
tradeStatistics3Fuzzed = TradeStatistics3.from(trade, referralId, isTorNetworkNode, true); tradeStatistics3V1 = TradeStatistics3.fromV1(trade, referralId, isTorNetworkNode);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage()); log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage());
return; return;
} }
boolean hasTradeStatistics3 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3.getHash())); TradeStatistics3 tradeStatistics3V2 = null;
boolean hasTradeStatistics3Fuzzed = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3Fuzzed.getHash())); try {
if (hasTradeStatistics3 || hasTradeStatistics3Fuzzed) { tradeStatistics3V2 = TradeStatistics3.fromV2(trade, referralId, isTorNetworkNode);
} catch (Exception e) {
log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage());
return;
}
boolean hasTradeStatistics3V0 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3V0.getHash()));
boolean hasTradeStatistics3V1 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3V1.getHash()));
boolean hasTradeStatistics3V2 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3V2.getHash()));
if (hasTradeStatistics3V0 || hasTradeStatistics3V1 || hasTradeStatistics3V2) {
log.debug("Trade: {}. We have already a tradeStatistics matching the hash of tradeStatistics3.", log.debug("Trade: {}. We have already a tradeStatistics matching the hash of tradeStatistics3.",
trade.getShortId()); trade.getShortId());
return; return;
} }
if (!tradeStatistics3.isValid()) { if (!tradeStatistics3V2.isValid()) {
log.warn("Trade: {}. Trade statistics is invalid. We do not publish it.", tradeStatistics3); log.warn("Trade statistics are invalid for {} {}. We do not publish: {}", trade.getClass().getSimpleName(), trade.getShortId(), tradeStatistics3V1);
return; return;
} }
log.info("Trade: {}. We republish tradeStatistics3 as we did not find it in the existing trade statistics. ", log.info("Publishing trade statistics for {} {}", trade.getClass().getSimpleName(), trade.getShortId());
trade.getShortId()); p2PService.addPersistableNetworkPayload(tradeStatistics3V2, true);
p2PService.addPersistableNetworkPayload(tradeStatistics3, true);
}); });
log.info("maybeRepublishTradeStatistics took {} ms. Number of tradeStatistics: {}. Number of own trades: {}", log.info("maybeRepublishTradeStatistics took {} ms. Number of tradeStatistics: {}. Number of own trades: {}",
System.currentTimeMillis() - ts, hashes.size(), trades.size()); System.currentTimeMillis() - ts, hashes.size(), trades.size());