From df3c68679f20ea3c959a04a3a990bae670ec3f5f Mon Sep 17 00:00:00 2001 From: CalDescent Date: Mon, 8 May 2023 14:43:00 +0100 Subject: [PATCH 01/13] Log the action to the console, instead of the entire event. --- src/main/resources/q-apps/q-apps.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/q-apps/q-apps.js b/src/main/resources/q-apps/q-apps.js index 86493b48..a505c1b0 100644 --- a/src/main/resources/q-apps/q-apps.js +++ b/src/main/resources/q-apps/q-apps.js @@ -169,7 +169,7 @@ window.addEventListener("message", (event) => { return; } - console.log("Core received event: " + JSON.stringify(event.data)); + console.log("Core received action: " + JSON.stringify(event.data.action)); let url; let data = event.data; From 49063e54ece7d94a60a3ff90ddbc28b15d34899b Mon Sep 17 00:00:00 2001 From: CalDescent Date: Mon, 8 May 2023 19:18:38 +0100 Subject: [PATCH 02/13] Bump version to 4.0.3 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 78df68a7..0dfa0cf4 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.qortal qortal - 4.0.2 + 4.0.3 jar true From cda32a47f182aca1f2563fc5d10f7fbd01e66294 Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Mon, 8 May 2023 20:23:54 -0400 Subject: [PATCH 03/13] Added API call to get votes --- .../qortal/api/resource/PollsResource.java | 30 +++++++++++++++++++ .../data/transaction/TransactionData.java | 3 +- .../qortal/data/voting/VoteOnPollData.java | 17 +++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/api/resource/PollsResource.java b/src/main/java/org/qortal/api/resource/PollsResource.java index 952cbdc5..ab163342 100644 --- a/src/main/java/org/qortal/api/resource/PollsResource.java +++ b/src/main/java/org/qortal/api/resource/PollsResource.java @@ -37,6 +37,7 @@ import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; import org.qortal.api.ApiException; import org.qortal.data.voting.PollData; +import org.qortal.data.voting.VoteOnPollData; @Path("/polls") @Tag(name = "Polls") @@ -102,6 +103,35 @@ public class PollsResource { } } + @GET + @Path("/votes/{pollName}") + @Operation( + summary = "Votes on poll", + responses = { + @ApiResponse( + description = "poll votes", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = VoteOnPollData.class) + ) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public List getVoteOnPollData(@PathParam("pollName") String pollName) { + try (final Repository repository = RepositoryManager.getRepository()) { + if (repository.getVotingRepository().fromPollName(pollName) == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.POLL_NO_EXISTS); + + List voteOnPollData = repository.getVotingRepository().getVotes(pollName); + return voteOnPollData; + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + @POST @Path("/create") @Operation( diff --git a/src/main/java/org/qortal/data/transaction/TransactionData.java b/src/main/java/org/qortal/data/transaction/TransactionData.java index 838cffd3..4bf3152c 100644 --- a/src/main/java/org/qortal/data/transaction/TransactionData.java +++ b/src/main/java/org/qortal/data/transaction/TransactionData.java @@ -13,6 +13,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorNode; import org.qortal.crypto.Crypto; import org.qortal.data.voting.PollData; +import org.qortal.data.voting.VoteOnPollData; import org.qortal.transaction.Transaction.ApprovalStatus; import org.qortal.transaction.Transaction.TransactionType; @@ -30,7 +31,7 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode; @XmlSeeAlso({GenesisTransactionData.class, PaymentTransactionData.class, RegisterNameTransactionData.class, UpdateNameTransactionData.class, SellNameTransactionData.class, CancelSellNameTransactionData.class, BuyNameTransactionData.class, CreatePollTransactionData.class, VoteOnPollTransactionData.class, ArbitraryTransactionData.class, - PollData.class, + PollData.class, VoteOnPollData.class, IssueAssetTransactionData.class, TransferAssetTransactionData.class, CreateAssetOrderTransactionData.class, CancelAssetOrderTransactionData.class, MultiPaymentTransactionData.class, DeployAtTransactionData.class, MessageTransactionData.class, ATTransactionData.class, diff --git a/src/main/java/org/qortal/data/voting/VoteOnPollData.java b/src/main/java/org/qortal/data/voting/VoteOnPollData.java index 47c06a54..531ed286 100644 --- a/src/main/java/org/qortal/data/voting/VoteOnPollData.java +++ b/src/main/java/org/qortal/data/voting/VoteOnPollData.java @@ -9,6 +9,11 @@ public class VoteOnPollData { // Constructors + // For JAXB + protected VoteOnPollData() { + super(); + } + public VoteOnPollData(String pollName, byte[] voterPublicKey, int optionIndex) { this.pollName = pollName; this.voterPublicKey = voterPublicKey; @@ -21,12 +26,24 @@ public class VoteOnPollData { return this.pollName; } + public void setPollName(String pollName) { + this.pollName = pollName; + } + public byte[] getVoterPublicKey() { return this.voterPublicKey; } + public void setVoterPublicKey(byte[] voterPublicKey) { + this.voterPublicKey = voterPublicKey; + } + public int getOptionIndex() { return this.optionIndex; } + public void setOptionIndex(int optionIndex) { + this.optionIndex = optionIndex; + } + } From 49c0d45bc6ec11766bf89844e0f4b320ffa0be23 Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Mon, 8 May 2023 23:26:23 -0400 Subject: [PATCH 04/13] Added count to get votes API call --- .../java/org/qortal/api/model/PollVotes.java | 56 +++++++++++++++++++ .../qortal/api/resource/PollsResource.java | 37 ++++++++++-- 2 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/qortal/api/model/PollVotes.java diff --git a/src/main/java/org/qortal/api/model/PollVotes.java b/src/main/java/org/qortal/api/model/PollVotes.java new file mode 100644 index 00000000..c57ebc37 --- /dev/null +++ b/src/main/java/org/qortal/api/model/PollVotes.java @@ -0,0 +1,56 @@ +package org.qortal.api.model; + +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; + +import io.swagger.v3.oas.annotations.media.Schema; + +import org.qortal.data.voting.VoteOnPollData; + +@Schema(description = "Poll vote info, including voters") +// All properties to be converted to JSON via JAX-RS +@XmlAccessorType(XmlAccessType.FIELD) +public class PollVotes { + + @Schema(description = "List of individual votes") + @XmlElement(name = "votes") + public List votes; + + @Schema(description = "Total number of votes") + public Integer totalVotes; + + @Schema(description = "List of vote counts for each option") + public List voteCounts; + + // For JAX-RS + protected PollVotes() { + } + + public PollVotes(List votes, Integer totalVotes, List voteCounts) { + this.votes = votes; + this.totalVotes = totalVotes; + this.voteCounts = voteCounts; + } + + @Schema(description = "Vote info") + // All properties to be converted to JSON via JAX-RS + @XmlAccessorType(XmlAccessType.FIELD) + public static class OptionCount { + @Schema(description = "Option name") + public String optionName; + + @Schema(description = "Vote count") + public Integer voteCount; + + // For JAX-RS + protected OptionCount() { + } + + public OptionCount(String optionName, Integer voteCount) { + this.optionName = optionName; + this.voteCount = voteCount; + } + } +} diff --git a/src/main/java/org/qortal/api/resource/PollsResource.java b/src/main/java/org/qortal/api/resource/PollsResource.java index ab163342..999fa2fd 100644 --- a/src/main/java/org/qortal/api/resource/PollsResource.java +++ b/src/main/java/org/qortal/api/resource/PollsResource.java @@ -31,12 +31,17 @@ import javax.ws.rs.core.MediaType; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import javax.ws.rs.GET; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; import org.qortal.api.ApiException; +import org.qortal.api.model.PollVotes; import org.qortal.data.voting.PollData; +import org.qortal.data.voting.PollOptionData; import org.qortal.data.voting.VoteOnPollData; @Path("/polls") @@ -112,19 +117,41 @@ public class PollsResource { description = "poll votes", content = @Content( mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = VoteOnPollData.class) + schema = @Schema(implementation = PollVotes.class) ) ) } ) @ApiErrors({ApiError.REPOSITORY_ISSUE}) - public List getVoteOnPollData(@PathParam("pollName") String pollName) { + public PollVotes getPollVotes(@PathParam("pollName") String pollName) { try (final Repository repository = RepositoryManager.getRepository()) { - if (repository.getVotingRepository().fromPollName(pollName) == null) + PollData pollData = repository.getVotingRepository().fromPollName(pollName); + if (pollData == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.POLL_NO_EXISTS); - List voteOnPollData = repository.getVotingRepository().getVotes(pollName); - return voteOnPollData; + List votes = repository.getVotingRepository().getVotes(pollName); + + // Initialize map for counting votes + Map voteCountMap = new HashMap<>(); + for (PollOptionData optionData : pollData.getPollOptions()) { + voteCountMap.put(optionData.getOptionName(), 0); + } + + int totalVotes = 0; + for (VoteOnPollData vote : votes) { + String selectedOption = pollData.getPollOptions().get(vote.getOptionIndex()).getOptionName(); + if (voteCountMap.containsKey(selectedOption)) { + voteCountMap.put(selectedOption, voteCountMap.get(selectedOption) + 1); + totalVotes++; + } + } + + // Convert map to list of VoteInfo + List voteCounts = voteCountMap.entrySet().stream() + .map(entry -> new PollVotes.OptionCount(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + + return new PollVotes(votes, totalVotes, voteCounts); } catch (ApiException e) { throw e; } catch (DataException e) { From 3e45948646c3e9fa22108c898c7334c51605314c Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Mon, 8 May 2023 23:41:31 -0400 Subject: [PATCH 05/13] Added get votes option to return only counts --- src/main/java/org/qortal/api/resource/PollsResource.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/PollsResource.java b/src/main/java/org/qortal/api/resource/PollsResource.java index 999fa2fd..c64a8caf 100644 --- a/src/main/java/org/qortal/api/resource/PollsResource.java +++ b/src/main/java/org/qortal/api/resource/PollsResource.java @@ -123,7 +123,7 @@ public class PollsResource { } ) @ApiErrors({ApiError.REPOSITORY_ISSUE}) - public PollVotes getPollVotes(@PathParam("pollName") String pollName) { + public PollVotes getPollVotes(@PathParam("pollName") String pollName, @QueryParam("onlyCounts") Boolean onlyCounts) { try (final Repository repository = RepositoryManager.getRepository()) { PollData pollData = repository.getVotingRepository().fromPollName(pollName); if (pollData == null) @@ -151,7 +151,11 @@ public class PollsResource { .map(entry -> new PollVotes.OptionCount(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); - return new PollVotes(votes, totalVotes, voteCounts); + if (onlyCounts != null && onlyCounts) { + return new PollVotes(null, totalVotes, voteCounts); + } else { + return new PollVotes(votes, totalVotes, voteCounts); + } } catch (ApiException e) { throw e; } catch (DataException e) { From e3be43a1e6456c3866aeaf82a3acf770a4b60ef2 Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Thu, 11 May 2023 12:31:00 -0400 Subject: [PATCH 06/13] Changed get name API call to use reduced name --- src/main/java/org/qortal/api/resource/NamesResource.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/NamesResource.java b/src/main/java/org/qortal/api/resource/NamesResource.java index 03dffc08..30f04b70 100644 --- a/src/main/java/org/qortal/api/resource/NamesResource.java +++ b/src/main/java/org/qortal/api/resource/NamesResource.java @@ -47,6 +47,7 @@ import org.qortal.transform.transaction.RegisterNameTransactionTransformer; import org.qortal.transform.transaction.SellNameTransactionTransformer; import org.qortal.transform.transaction.UpdateNameTransactionTransformer; import org.qortal.utils.Base58; +import org.qortal.utils.Unicode; @Path("/names") @Tag(name = "Names") @@ -135,12 +136,13 @@ public class NamesResource { public NameData getName(@PathParam("name") String name) { try (final Repository repository = RepositoryManager.getRepository()) { NameData nameData; + String reducedName = Unicode.sanitize(name); if (Settings.getInstance().isLite()) { nameData = LiteNode.getInstance().fetchNameData(name); } else { - nameData = repository.getNameRepository().fromName(name); + nameData = repository.getNameRepository().fromReducedName(reducedName); } if (nameData == null) { @@ -442,4 +444,4 @@ public class NamesResource { } } -} \ No newline at end of file +} From 2cbc5aabd53fcc323ec54cc9e811db1aaef525cf Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 12 May 2023 09:59:30 +0100 Subject: [PATCH 07/13] 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. --- .../api/resource/CrossChainResource.java | 3 + .../api/websocket/TradeOffersWebSocket.java | 20 +++-- .../qortal/controller/tradebot/TradeBot.java | 78 +++++++++++++++++++ .../java/org/qortal/settings/Settings.java | 7 ++ 4 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java index bb7c70a5..2a494db7 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -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()); diff --git a/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java b/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java index 78c53dc3..9c48b018 100644 --- a/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java +++ b/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java @@ -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 produceSummaries(Repository repository, ACCT acct, List atStates, Long timestamp) throws DataException { List 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; } diff --git a/src/main/java/org/qortal/controller/tradebot/TradeBot.java b/src/main/java/org/qortal/controller/tradebot/TradeBot.java index 5880f561..96eeaf36 100644 --- a/src/main/java/org/qortal/controller/tradebot/TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/TradeBot.java @@ -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 safeAllTradePresencesByPubkey = Collections.emptyMap(); private long nextTradePresenceBroadcastTimestamp = 0L; + private Map failedTrades = new HashMap<>(); + private Map 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 removeFailedTrades(Repository repository, List crossChainTrades) { + Long now = NTP.getTime(); + if (now == null) { + return crossChainTrades; + } + + List 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 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 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 results = removeFailedTrades(repository, Arrays.asList(crossChainTradeData)); + return results.isEmpty(); + } + private long generateExpiry(long timestamp) { return ((timestamp - 1) / EXPIRY_ROUNDING) * EXPIRY_ROUNDING + PRESENCE_LIFETIME; } diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 6b703bea..a87a72f4 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -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; } From ba4866a2e65c08f0dcb7aa0c206a77ce4abd019a Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 12 May 2023 10:01:38 +0100 Subject: [PATCH 08/13] Added `GET /crosschain/tradeoffers/hidden` endpoint, to show offers that are currently being hidden. This uses the maxTradeOfferAttempts setting, so modifying this setting will affect the number of offers that are returned. --- .../api/resource/CrossChainResource.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java index 2a494db7..44ef62ad 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -132,6 +132,64 @@ public class CrossChainResource { } } + @GET + @Path("/tradeoffers/hidden") + @Operation( + summary = "Find cross-chain trade offers that have been hidden due to too many failures", + responses = { + @ApiResponse( + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = CrossChainTradeData.class + ) + ) + ) + ) + } + ) + @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE}) + public List getHiddenTradeOffers( + @Parameter( + description = "Limit to specific blockchain", + example = "LITECOIN", + schema = @Schema(implementation = SupportedBlockchain.class) + ) @QueryParam("foreignBlockchain") SupportedBlockchain foreignBlockchain) { + + final boolean isExecutable = true; + List crossChainTrades = new ArrayList<>(); + + try (final Repository repository = RepositoryManager.getRepository()) { + Map> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(foreignBlockchain); + + for (Map.Entry> acctInfo : acctsByCodeHash.entrySet()) { + byte[] codeHash = acctInfo.getKey().value; + ACCT acct = acctInfo.getValue().get(); + + List atsData = repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, null, null, null); + + for (ATData atData : atsData) { + CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData); + if (crossChainTradeData.mode == AcctMode.OFFERING) { + crossChainTrades.add(crossChainTradeData); + } + } + } + + // Sort the trades by timestamp + crossChainTrades.sort((a, b) -> Longs.compare(a.creationTimestamp, b.creationTimestamp)); + + // Remove trades that haven't failed + crossChainTrades.removeIf(t -> !TradeBot.getInstance().isFailedTrade(repository, t)); + + crossChainTrades.stream().forEach(CrossChainResource::decorateTradeDataWithPresence); + + return crossChainTrades; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + @GET @Path("/trade/{ataddress}") @Operation( From dc1289787db0f7398f9eb0f44225c8da946dac7c Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 12 May 2023 10:12:38 +0100 Subject: [PATCH 09/13] Ignore per-name limits when using storagePolicy ALL. --- .../controller/arbitrary/ArbitraryDataStorageManager.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataStorageManager.java b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataStorageManager.java index 8b7d1a69..d3aadc43 100644 --- a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataStorageManager.java +++ b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataStorageManager.java @@ -488,6 +488,11 @@ public class ArbitraryDataStorageManager extends Thread { return false; } + if (Settings.getInstance().getStoragePolicy() == StoragePolicy.ALL) { + // Using storage policy ALL, so don't limit anything per name + return true; + } + if (name == null) { // This transaction doesn't have a name, so fall back to total space limitations return true; From 5a873f946509d0524d30ce01a54abecb981c4676 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 12 May 2023 11:11:34 +0100 Subject: [PATCH 10/13] Added `prefix` parameter to `GET /names/search`. --- src/main/java/org/qortal/api/resource/NamesResource.java | 5 ++++- src/main/java/org/qortal/repository/NameRepository.java | 2 +- .../org/qortal/repository/hsqldb/HSQLDBNameRepository.java | 7 +++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/NamesResource.java b/src/main/java/org/qortal/api/resource/NamesResource.java index 30f04b70..7627c413 100644 --- a/src/main/java/org/qortal/api/resource/NamesResource.java +++ b/src/main/java/org/qortal/api/resource/NamesResource.java @@ -173,6 +173,7 @@ public class NamesResource { ) @ApiErrors({ApiError.NAME_UNKNOWN, ApiError.REPOSITORY_ISSUE}) public List searchNames(@QueryParam("query") String query, + @Parameter(description = "Prefix only (if true, only the beginning of the name is matched)") @QueryParam("prefix") Boolean prefixOnly, @Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref="reverse") @QueryParam("reverse") Boolean reverse) { @@ -181,7 +182,9 @@ public class NamesResource { throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Missing query"); } - return repository.getNameRepository().searchNames(query, limit, offset, reverse); + boolean usePrefixOnly = Boolean.TRUE.equals(prefixOnly); + + return repository.getNameRepository().searchNames(query, usePrefixOnly, limit, offset, reverse); } catch (ApiException e) { throw e; } catch (DataException e) { diff --git a/src/main/java/org/qortal/repository/NameRepository.java b/src/main/java/org/qortal/repository/NameRepository.java index a8b2a3db..32097ca4 100644 --- a/src/main/java/org/qortal/repository/NameRepository.java +++ b/src/main/java/org/qortal/repository/NameRepository.java @@ -14,7 +14,7 @@ public interface NameRepository { public boolean reducedNameExists(String reducedName) throws DataException; - public List searchNames(String query, Integer limit, Integer offset, Boolean reverse) throws DataException; + public List searchNames(String query, boolean prefixOnly, Integer limit, Integer offset, Boolean reverse) throws DataException; public List getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java index 3e4a8e11..40f123d1 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java @@ -103,7 +103,7 @@ public class HSQLDBNameRepository implements NameRepository { } } - public List searchNames(String query, Integer limit, Integer offset, Boolean reverse) throws DataException { + public List searchNames(String query, boolean prefixOnly, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); List bindParams = new ArrayList<>(); @@ -111,7 +111,10 @@ public class HSQLDBNameRepository implements NameRepository { + "is_for_sale, sale_price, reference, creation_group_id FROM Names " + "WHERE LCASE(name) LIKE ? ORDER BY name"); - bindParams.add(String.format("%%%s%%", query.toLowerCase())); + // Search anywhere in the name, unless "prefixOnly" has been requested + // Note that without prefixOnly it will bypass any indexes + String queryWildcard = prefixOnly ? String.format("%s%%", query.toLowerCase()) : String.format("%%%s%%", query.toLowerCase()); + bindParams.add(queryWildcard); if (reverse != null && reverse) sql.append(" DESC"); From 29480e56649c87899cd0070ac67ea4c0b4dcb71e Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 12 May 2023 11:17:09 +0100 Subject: [PATCH 11/13] Added SEARCH_NAMES Q-App action. --- Q-Apps.md | 13 +++++++++++++ src/main/resources/q-apps/q-apps.js | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/Q-Apps.md b/Q-Apps.md index 177fee2d..ca750e7d 100644 --- a/Q-Apps.md +++ b/Q-Apps.md @@ -252,6 +252,7 @@ Here is a list of currently supported actions: - GET_USER_ACCOUNT - GET_ACCOUNT_DATA - GET_ACCOUNT_NAMES +- SEARCH_NAMES - GET_NAME_DATA - LIST_QDN_RESOURCES - SEARCH_QDN_RESOURCES @@ -324,6 +325,18 @@ let res = await qortalRequest({ }); ``` +### Search names +``` +let res = await qortalRequest({ + action: "SEARCH_NAMES", + query: "search query goes here", + prefix: false, // Optional - if true, only the beginning of the name is matched + limit: 100, + offset: 0, + reverse: false +}); +``` + ### Get name data ``` let res = await qortalRequest({ diff --git a/src/main/resources/q-apps/q-apps.js b/src/main/resources/q-apps/q-apps.js index a505c1b0..dae20e5d 100644 --- a/src/main/resources/q-apps/q-apps.js +++ b/src/main/resources/q-apps/q-apps.js @@ -181,6 +181,15 @@ window.addEventListener("message", (event) => { case "GET_ACCOUNT_NAMES": return httpGetAsyncWithEvent(event, "/names/address/" + data.address); + case "SEARCH_NAMES": + url = "/names/search?"; + if (data.query != null) url = url.concat("&query=" + data.query); + if (data.prefix != null) url = url.concat("&prefix=" + new Boolean(data.prefix).toString()); + if (data.limit != null) url = url.concat("&limit=" + data.limit); + if (data.offset != null) url = url.concat("&offset=" + data.offset); + if (data.reverse != null) url = url.concat("&reverse=" + new Boolean(data.reverse).toString()); + return httpGetAsyncWithEvent(event, url); + case "GET_NAME_DATA": return httpGetAsyncWithEvent(event, "/names/" + data.name); From f8233bd05b9d0d040a67d6b4b844d6d20779701d Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 12 May 2023 11:41:00 +0100 Subject: [PATCH 12/13] Added optional `after` parameter to `GET /names`. --- .../org/qortal/api/resource/NamesResource.java | 8 +++++--- .../org/qortal/repository/NameRepository.java | 4 ++-- .../repository/hsqldb/HSQLDBNameRepository.java | 15 ++++++++++++--- .../java/org/qortal/test/api/NamesApiTests.java | 4 ++-- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/NamesResource.java b/src/main/java/org/qortal/api/resource/NamesResource.java index 7627c413..4173b85b 100644 --- a/src/main/java/org/qortal/api/resource/NamesResource.java +++ b/src/main/java/org/qortal/api/resource/NamesResource.java @@ -70,10 +70,12 @@ public class NamesResource { } ) @ApiErrors({ApiError.REPOSITORY_ISSUE}) - public List getAllNames(@Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, - @Parameter(ref="reverse") @QueryParam("reverse") Boolean reverse) { + public List getAllNames(@Parameter(ref = "after") @QueryParam("after") Long after, + @Parameter(ref = "limit") @QueryParam("limit") Integer limit, + @Parameter(ref = "offset") @QueryParam("offset") Integer offset, + @Parameter(ref="reverse") @QueryParam("reverse") Boolean reverse) { try (final Repository repository = RepositoryManager.getRepository()) { - List names = repository.getNameRepository().getAllNames(limit, offset, reverse); + List names = repository.getNameRepository().getAllNames(after, limit, offset, reverse); // Convert to summary return names.stream().map(NameSummary::new).collect(Collectors.toList()); diff --git a/src/main/java/org/qortal/repository/NameRepository.java b/src/main/java/org/qortal/repository/NameRepository.java index 32097ca4..52a43a18 100644 --- a/src/main/java/org/qortal/repository/NameRepository.java +++ b/src/main/java/org/qortal/repository/NameRepository.java @@ -16,10 +16,10 @@ public interface NameRepository { public List searchNames(String query, boolean prefixOnly, Integer limit, Integer offset, Boolean reverse) throws DataException; - public List getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException; + public List getAllNames(Long after, Integer limit, Integer offset, Boolean reverse) throws DataException; public default List getAllNames() throws DataException { - return getAllNames(null, null, null); + return getAllNames(null, null, null, null); } public List getNamesForSale(Integer limit, Integer offset, Boolean reverse) throws DataException; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java index 40f123d1..2fefcf8b 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java @@ -158,11 +158,20 @@ public class HSQLDBNameRepository implements NameRepository { } @Override - public List getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException { + public List getAllNames(Long after, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); + List bindParams = new ArrayList<>(); sql.append("SELECT name, reduced_name, owner, data, registered_when, updated_when, " - + "is_for_sale, sale_price, reference, creation_group_id FROM Names ORDER BY name"); + + "is_for_sale, sale_price, reference, creation_group_id FROM Names"); + + if (after != null) { + sql.append(" WHERE registered_when > ? OR updated_when > ?"); + bindParams.add(after); + bindParams.add(after); + } + + sql.append(" ORDER BY name"); if (reverse != null && reverse) sql.append(" DESC"); @@ -171,7 +180,7 @@ public class HSQLDBNameRepository implements NameRepository { List names = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) { + try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) { if (resultSet == null) return names; diff --git a/src/test/java/org/qortal/test/api/NamesApiTests.java b/src/test/java/org/qortal/test/api/NamesApiTests.java index 0e03b6a6..effdfea4 100644 --- a/src/test/java/org/qortal/test/api/NamesApiTests.java +++ b/src/test/java/org/qortal/test/api/NamesApiTests.java @@ -37,8 +37,8 @@ public class NamesApiTests extends ApiCommon { @Test public void testGetAllNames() { - assertNotNull(this.namesResource.getAllNames(null, null, null)); - assertNotNull(this.namesResource.getAllNames(1, 1, true)); + assertNotNull(this.namesResource.getAllNames(null, null, null, null)); + assertNotNull(this.namesResource.getAllNames(1L, 1, 1, true)); } @Test From 8a1bf8b5ecbb35f8026862d496d172a11ec7212f Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 12 May 2023 11:41:15 +0100 Subject: [PATCH 13/13] Return full name data in `GET /names`. --- src/main/java/org/qortal/api/resource/NamesResource.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/NamesResource.java b/src/main/java/org/qortal/api/resource/NamesResource.java index 4173b85b..6cde26b3 100644 --- a/src/main/java/org/qortal/api/resource/NamesResource.java +++ b/src/main/java/org/qortal/api/resource/NamesResource.java @@ -64,21 +64,19 @@ public class NamesResource { description = "registered name info", content = @Content( mediaType = MediaType.APPLICATION_JSON, - array = @ArraySchema(schema = @Schema(implementation = NameSummary.class)) + array = @ArraySchema(schema = @Schema(implementation = NameData.class)) ) ) } ) @ApiErrors({ApiError.REPOSITORY_ISSUE}) - public List getAllNames(@Parameter(ref = "after") @QueryParam("after") Long after, + public List getAllNames(@Parameter(description = "Return only names registered or updated after timestamp") @QueryParam("after") Long after, @Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref="reverse") @QueryParam("reverse") Boolean reverse) { try (final Repository repository = RepositoryManager.getRepository()) { - List names = repository.getNameRepository().getAllNames(after, limit, offset, reverse); - // Convert to summary - return names.stream().map(NameSummary::new).collect(Collectors.toList()); + return repository.getNameRepository().getAllNames(after, limit, offset, reverse); } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); }