forked from Qortal/qortal
Added maxTradeOfferAttempts setting (default 3).
Offers with more than 3 failures will be hidden from the API and websocket, to prevent unbuyable offers from staying in the order books and continuously failing. maxTradeOfferAttempts can be optionally increased on a node to show more trades that would otherwise be hidden.
This commit is contained in:
parent
e3be43a1e6
commit
2cbc5aabd5
@ -115,6 +115,9 @@ public class CrossChainResource {
|
||||
crossChainTrades.sort((a, b) -> Longs.compare(a.creationTimestamp, b.creationTimestamp));
|
||||
}
|
||||
|
||||
// Remove any trades that have had too many failures
|
||||
crossChainTrades = TradeBot.getInstance().removeFailedTrades(repository, crossChainTrades);
|
||||
|
||||
if (limit != null && limit > 0) {
|
||||
// Make sure to not return more than the limit
|
||||
int upperLimit = Math.min(limit, crossChainTrades.size());
|
||||
|
@ -24,6 +24,7 @@ import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.api.model.CrossChainOfferSummary;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.controller.tradebot.TradeBot;
|
||||
import org.qortal.crosschain.SupportedBlockchain;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
@ -315,7 +316,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
||||
throw new DataException("Couldn't fetch historic trades from repository");
|
||||
|
||||
for (ATStateData historicAtState : historicAtStates) {
|
||||
CrossChainOfferSummary historicOfferSummary = produceSummary(repository, acct, historicAtState, null);
|
||||
CrossChainOfferSummary historicOfferSummary = produceSummary(repository, acct, historicAtState, null, null);
|
||||
|
||||
if (!isHistoric.test(historicOfferSummary))
|
||||
continue;
|
||||
@ -330,8 +331,10 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
private static CrossChainOfferSummary produceSummary(Repository repository, ACCT acct, ATStateData atState, Long timestamp) throws DataException {
|
||||
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
|
||||
private static CrossChainOfferSummary produceSummary(Repository repository, ACCT acct, ATStateData atState, CrossChainTradeData crossChainTradeData, Long timestamp) throws DataException {
|
||||
if (crossChainTradeData == null) {
|
||||
crossChainTradeData = acct.populateTradeData(repository, atState);
|
||||
}
|
||||
|
||||
long atStateTimestamp;
|
||||
|
||||
@ -346,9 +349,16 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
||||
|
||||
private static List<CrossChainOfferSummary> produceSummaries(Repository repository, ACCT acct, List<ATStateData> atStates, Long timestamp) throws DataException {
|
||||
List<CrossChainOfferSummary> offerSummaries = new ArrayList<>();
|
||||
for (ATStateData atState : atStates) {
|
||||
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
|
||||
|
||||
for (ATStateData atState : atStates)
|
||||
offerSummaries.add(produceSummary(repository, acct, atState, timestamp));
|
||||
// Ignore trade if it has failed
|
||||
if (TradeBot.getInstance().isFailedTrade(repository, crossChainTradeData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
offerSummaries.add(produceSummary(repository, acct, atState, crossChainTradeData, timestamp));
|
||||
}
|
||||
|
||||
return offerSummaries;
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import org.apache.logging.log4j.Logger;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||
import org.qortal.api.resource.TransactionsResource;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.controller.tradebot.AcctTradeBot.ResponseResult;
|
||||
@ -19,6 +20,7 @@ import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.crosschain.TradeBotData;
|
||||
import org.qortal.data.network.TradePresenceData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.event.Event;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.event.Listener;
|
||||
@ -33,6 +35,7 @@ import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.repository.hsqldb.HSQLDBImportExport;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.utils.ByteArray;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@ -113,6 +116,9 @@ public class TradeBot implements Listener {
|
||||
private Map<ByteArray, TradePresenceData> safeAllTradePresencesByPubkey = Collections.emptyMap();
|
||||
private long nextTradePresenceBroadcastTimestamp = 0L;
|
||||
|
||||
private Map<String, Long> failedTrades = new HashMap<>();
|
||||
private Map<String, Long> validTrades = new HashMap<>();
|
||||
|
||||
private TradeBot() {
|
||||
EventBus.INSTANCE.addListener(event -> TradeBot.getInstance().listen(event));
|
||||
}
|
||||
@ -674,6 +680,78 @@ public class TradeBot implements Listener {
|
||||
});
|
||||
}
|
||||
|
||||
/** Removes any trades that have had multiple failures */
|
||||
public List<CrossChainTradeData> removeFailedTrades(Repository repository, List<CrossChainTradeData> crossChainTrades) {
|
||||
Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
return crossChainTrades;
|
||||
}
|
||||
|
||||
List<CrossChainTradeData> updatedCrossChainTrades = new ArrayList<>(crossChainTrades);
|
||||
int getMaxTradeOfferAttempts = Settings.getInstance().getMaxTradeOfferAttempts();
|
||||
|
||||
for (CrossChainTradeData crossChainTradeData : crossChainTrades) {
|
||||
// We only care about trades in the OFFERING state
|
||||
if (crossChainTradeData.mode != AcctMode.OFFERING) {
|
||||
failedTrades.remove(crossChainTradeData.qortalAtAddress);
|
||||
validTrades.remove(crossChainTradeData.qortalAtAddress);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Return recently cached values if they exist
|
||||
Long failedTimestamp = failedTrades.get(crossChainTradeData.qortalAtAddress);
|
||||
if (failedTimestamp != null && now - failedTimestamp < 60 * 60 * 1000L) {
|
||||
updatedCrossChainTrades.remove(crossChainTradeData);
|
||||
//LOGGER.info("Removing cached failed trade AT {}", crossChainTradeData.qortalAtAddress);
|
||||
continue;
|
||||
}
|
||||
Long validTimestamp = validTrades.get(crossChainTradeData.qortalAtAddress);
|
||||
if (validTimestamp != null && now - validTimestamp < 60 * 60 * 1000L) {
|
||||
//LOGGER.info("NOT removing cached valid trade AT {}", crossChainTradeData.qortalAtAddress);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, Arrays.asList(Transaction.TransactionType.MESSAGE), null, null, crossChainTradeData.qortalCreatorTradeAddress, TransactionsResource.ConfirmationStatus.CONFIRMED, null, null, null);
|
||||
if (signatures.size() < getMaxTradeOfferAttempts) {
|
||||
// Less than 3 (or user-specified number of) MESSAGE transactions relate to this trade, so assume it is ok
|
||||
validTrades.put(crossChainTradeData.qortalAtAddress, now);
|
||||
continue;
|
||||
}
|
||||
|
||||
List<TransactionData> transactions = new ArrayList<>(signatures.size());
|
||||
for (byte[] signature : signatures) {
|
||||
transactions.add(repository.getTransactionRepository().fromSignature(signature));
|
||||
}
|
||||
transactions.sort(Transaction.getDataComparator());
|
||||
|
||||
// Get timestamp of the first MESSAGE transaction
|
||||
long firstMessageTimestamp = transactions.get(0).getTimestamp();
|
||||
|
||||
// Treat as failed if first buy attempt was more than 60 mins ago (as it's still in the OFFERING state)
|
||||
boolean isFailed = (now - firstMessageTimestamp > 60*60*1000L);
|
||||
if (isFailed) {
|
||||
failedTrades.put(crossChainTradeData.qortalAtAddress, now);
|
||||
updatedCrossChainTrades.remove(crossChainTradeData);
|
||||
}
|
||||
else {
|
||||
validTrades.put(crossChainTradeData.qortalAtAddress, now);
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("Unable to determine failed state of AT {}", crossChainTradeData.qortalAtAddress);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return updatedCrossChainTrades;
|
||||
}
|
||||
|
||||
public boolean isFailedTrade(Repository repository, CrossChainTradeData crossChainTradeData) {
|
||||
List<CrossChainTradeData> results = removeFailedTrades(repository, Arrays.asList(crossChainTradeData));
|
||||
return results.isEmpty();
|
||||
}
|
||||
|
||||
private long generateExpiry(long timestamp) {
|
||||
return ((timestamp - 1) / EXPIRY_ROUNDING) * EXPIRY_ROUNDING + PRESENCE_LIFETIME;
|
||||
}
|
||||
|
@ -253,6 +253,9 @@ public class Settings {
|
||||
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
|
||||
private boolean tradebotSystrayEnabled = false;
|
||||
|
||||
/** Maximum buy attempts for each trade offer before it is considered failed, and hidden from the list */
|
||||
private int maxTradeOfferAttempts = 3;
|
||||
|
||||
/** Wallets path - used for storing encrypted wallet caches for coins that require them */
|
||||
private String walletsPath = "wallets";
|
||||
|
||||
@ -771,6 +774,10 @@ public class Settings {
|
||||
return this.pirateChainNet;
|
||||
}
|
||||
|
||||
public int getMaxTradeOfferAttempts() {
|
||||
return this.maxTradeOfferAttempts;
|
||||
}
|
||||
|
||||
public String getWalletsPath() {
|
||||
return this.walletsPath;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user