forked from Qortal/qortal
Merge branch 'master' into arbitrary-resources-cache
This commit is contained in:
commit
21f01226e9
13
Q-Apps.md
13
Q-Apps.md
@ -252,6 +252,7 @@ Here is a list of currently supported actions:
|
|||||||
- GET_USER_ACCOUNT
|
- GET_USER_ACCOUNT
|
||||||
- GET_ACCOUNT_DATA
|
- GET_ACCOUNT_DATA
|
||||||
- GET_ACCOUNT_NAMES
|
- GET_ACCOUNT_NAMES
|
||||||
|
- SEARCH_NAMES
|
||||||
- GET_NAME_DATA
|
- GET_NAME_DATA
|
||||||
- LIST_QDN_RESOURCES
|
- LIST_QDN_RESOURCES
|
||||||
- SEARCH_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
|
### Get name data
|
||||||
```
|
```
|
||||||
let res = await qortalRequest({
|
let res = await qortalRequest({
|
||||||
|
2
pom.xml
2
pom.xml
@ -3,7 +3,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>org.qortal</groupId>
|
<groupId>org.qortal</groupId>
|
||||||
<artifactId>qortal</artifactId>
|
<artifactId>qortal</artifactId>
|
||||||
<version>4.0.2</version>
|
<version>4.0.3</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<properties>
|
<properties>
|
||||||
<skipTests>true</skipTests>
|
<skipTests>true</skipTests>
|
||||||
|
56
src/main/java/org/qortal/api/model/PollVotes.java
Normal file
56
src/main/java/org/qortal/api/model/PollVotes.java
Normal file
@ -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<VoteOnPollData> votes;
|
||||||
|
|
||||||
|
@Schema(description = "Total number of votes")
|
||||||
|
public Integer totalVotes;
|
||||||
|
|
||||||
|
@Schema(description = "List of vote counts for each option")
|
||||||
|
public List<OptionCount> voteCounts;
|
||||||
|
|
||||||
|
// For JAX-RS
|
||||||
|
protected PollVotes() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public PollVotes(List<VoteOnPollData> votes, Integer totalVotes, List<OptionCount> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -115,6 +115,9 @@ public class CrossChainResource {
|
|||||||
crossChainTrades.sort((a, b) -> Longs.compare(a.creationTimestamp, b.creationTimestamp));
|
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) {
|
if (limit != null && limit > 0) {
|
||||||
// Make sure to not return more than the limit
|
// Make sure to not return more than the limit
|
||||||
int upperLimit = Math.min(limit, crossChainTrades.size());
|
int upperLimit = Math.min(limit, crossChainTrades.size());
|
||||||
@ -129,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<CrossChainTradeData> getHiddenTradeOffers(
|
||||||
|
@Parameter(
|
||||||
|
description = "Limit to specific blockchain",
|
||||||
|
example = "LITECOIN",
|
||||||
|
schema = @Schema(implementation = SupportedBlockchain.class)
|
||||||
|
) @QueryParam("foreignBlockchain") SupportedBlockchain foreignBlockchain) {
|
||||||
|
|
||||||
|
final boolean isExecutable = true;
|
||||||
|
List<CrossChainTradeData> crossChainTrades = new ArrayList<>();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(foreignBlockchain);
|
||||||
|
|
||||||
|
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
|
||||||
|
byte[] codeHash = acctInfo.getKey().value;
|
||||||
|
ACCT acct = acctInfo.getValue().get();
|
||||||
|
|
||||||
|
List<ATData> 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
|
@GET
|
||||||
@Path("/trade/{ataddress}")
|
@Path("/trade/{ataddress}")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -47,6 +47,7 @@ import org.qortal.transform.transaction.RegisterNameTransactionTransformer;
|
|||||||
import org.qortal.transform.transaction.SellNameTransactionTransformer;
|
import org.qortal.transform.transaction.SellNameTransactionTransformer;
|
||||||
import org.qortal.transform.transaction.UpdateNameTransactionTransformer;
|
import org.qortal.transform.transaction.UpdateNameTransactionTransformer;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.Unicode;
|
||||||
|
|
||||||
@Path("/names")
|
@Path("/names")
|
||||||
@Tag(name = "Names")
|
@Tag(name = "Names")
|
||||||
@ -63,19 +64,19 @@ public class NamesResource {
|
|||||||
description = "registered name info",
|
description = "registered name info",
|
||||||
content = @Content(
|
content = @Content(
|
||||||
mediaType = MediaType.APPLICATION_JSON,
|
mediaType = MediaType.APPLICATION_JSON,
|
||||||
array = @ArraySchema(schema = @Schema(implementation = NameSummary.class))
|
array = @ArraySchema(schema = @Schema(implementation = NameData.class))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||||
public List<NameSummary> getAllNames(@Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
public List<NameData> 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) {
|
@Parameter(ref="reverse") @QueryParam("reverse") Boolean reverse) {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
List<NameData> names = repository.getNameRepository().getAllNames(limit, offset, reverse);
|
|
||||||
|
|
||||||
// Convert to summary
|
return repository.getNameRepository().getAllNames(after, limit, offset, reverse);
|
||||||
return names.stream().map(NameSummary::new).collect(Collectors.toList());
|
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
}
|
}
|
||||||
@ -135,12 +136,13 @@ public class NamesResource {
|
|||||||
public NameData getName(@PathParam("name") String name) {
|
public NameData getName(@PathParam("name") String name) {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
NameData nameData;
|
NameData nameData;
|
||||||
|
String reducedName = Unicode.sanitize(name);
|
||||||
|
|
||||||
if (Settings.getInstance().isLite()) {
|
if (Settings.getInstance().isLite()) {
|
||||||
nameData = LiteNode.getInstance().fetchNameData(name);
|
nameData = LiteNode.getInstance().fetchNameData(name);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
nameData = repository.getNameRepository().fromName(name);
|
nameData = repository.getNameRepository().fromReducedName(reducedName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameData == null) {
|
if (nameData == null) {
|
||||||
@ -171,6 +173,7 @@ public class NamesResource {
|
|||||||
)
|
)
|
||||||
@ApiErrors({ApiError.NAME_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
@ApiErrors({ApiError.NAME_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||||
public List<NameData> searchNames(@QueryParam("query") String query,
|
public List<NameData> 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 = "limit") @QueryParam("limit") Integer limit,
|
||||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||||
@Parameter(ref="reverse") @QueryParam("reverse") Boolean reverse) {
|
@Parameter(ref="reverse") @QueryParam("reverse") Boolean reverse) {
|
||||||
@ -179,7 +182,9 @@ public class NamesResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Missing query");
|
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) {
|
} catch (ApiException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
|
@ -31,12 +31,18 @@ import javax.ws.rs.core.MediaType;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
import org.qortal.api.ApiException;
|
import org.qortal.api.ApiException;
|
||||||
|
import org.qortal.api.model.PollVotes;
|
||||||
import org.qortal.data.voting.PollData;
|
import org.qortal.data.voting.PollData;
|
||||||
|
import org.qortal.data.voting.PollOptionData;
|
||||||
|
import org.qortal.data.voting.VoteOnPollData;
|
||||||
|
|
||||||
@Path("/polls")
|
@Path("/polls")
|
||||||
@Tag(name = "Polls")
|
@Tag(name = "Polls")
|
||||||
@ -102,6 +108,61 @@ 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 = PollVotes.class)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||||
|
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)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.POLL_NO_EXISTS);
|
||||||
|
|
||||||
|
List<VoteOnPollData> votes = repository.getVotingRepository().getVotes(pollName);
|
||||||
|
|
||||||
|
// Initialize map for counting votes
|
||||||
|
Map<String, Integer> 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<PollVotes.OptionCount> voteCounts = voteCountMap.entrySet().stream()
|
||||||
|
.map(entry -> new PollVotes.OptionCount(entry.getKey(), entry.getValue()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
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) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/create")
|
@Path("/create")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -24,6 +24,7 @@ import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
|||||||
import org.qortal.api.model.CrossChainOfferSummary;
|
import org.qortal.api.model.CrossChainOfferSummary;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.controller.Synchronizer;
|
import org.qortal.controller.Synchronizer;
|
||||||
|
import org.qortal.controller.tradebot.TradeBot;
|
||||||
import org.qortal.crosschain.SupportedBlockchain;
|
import org.qortal.crosschain.SupportedBlockchain;
|
||||||
import org.qortal.crosschain.ACCT;
|
import org.qortal.crosschain.ACCT;
|
||||||
import org.qortal.crosschain.AcctMode;
|
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");
|
throw new DataException("Couldn't fetch historic trades from repository");
|
||||||
|
|
||||||
for (ATStateData historicAtState : historicAtStates) {
|
for (ATStateData historicAtState : historicAtStates) {
|
||||||
CrossChainOfferSummary historicOfferSummary = produceSummary(repository, acct, historicAtState, null);
|
CrossChainOfferSummary historicOfferSummary = produceSummary(repository, acct, historicAtState, null, null);
|
||||||
|
|
||||||
if (!isHistoric.test(historicOfferSummary))
|
if (!isHistoric.test(historicOfferSummary))
|
||||||
continue;
|
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 {
|
private static CrossChainOfferSummary produceSummary(Repository repository, ACCT acct, ATStateData atState, CrossChainTradeData crossChainTradeData, Long timestamp) throws DataException {
|
||||||
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
|
if (crossChainTradeData == null) {
|
||||||
|
crossChainTradeData = acct.populateTradeData(repository, atState);
|
||||||
|
}
|
||||||
|
|
||||||
long atStateTimestamp;
|
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 {
|
private static List<CrossChainOfferSummary> produceSummaries(Repository repository, ACCT acct, List<ATStateData> atStates, Long timestamp) throws DataException {
|
||||||
List<CrossChainOfferSummary> offerSummaries = new ArrayList<>();
|
List<CrossChainOfferSummary> offerSummaries = new ArrayList<>();
|
||||||
|
for (ATStateData atState : atStates) {
|
||||||
|
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
|
||||||
|
|
||||||
for (ATStateData atState : atStates)
|
// Ignore trade if it has failed
|
||||||
offerSummaries.add(produceSummary(repository, acct, atState, timestamp));
|
if (TradeBot.getInstance().isFailedTrade(repository, crossChainTradeData)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
offerSummaries.add(produceSummary(repository, acct, atState, crossChainTradeData, timestamp));
|
||||||
|
}
|
||||||
|
|
||||||
return offerSummaries;
|
return offerSummaries;
|
||||||
}
|
}
|
||||||
|
@ -488,6 +488,11 @@ public class ArbitraryDataStorageManager extends Thread {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Settings.getInstance().getStoragePolicy() == StoragePolicy.ALL) {
|
||||||
|
// Using storage policy ALL, so don't limit anything per name
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (name == null) {
|
if (name == null) {
|
||||||
// This transaction doesn't have a name, so fall back to total space limitations
|
// This transaction doesn't have a name, so fall back to total space limitations
|
||||||
return true;
|
return true;
|
||||||
|
@ -10,6 +10,7 @@ import org.apache.logging.log4j.Logger;
|
|||||||
import org.bitcoinj.core.ECKey;
|
import org.bitcoinj.core.ECKey;
|
||||||
import org.qortal.account.PrivateKeyAccount;
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||||
|
import org.qortal.api.resource.TransactionsResource;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.controller.Synchronizer;
|
import org.qortal.controller.Synchronizer;
|
||||||
import org.qortal.controller.tradebot.AcctTradeBot.ResponseResult;
|
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.CrossChainTradeData;
|
||||||
import org.qortal.data.crosschain.TradeBotData;
|
import org.qortal.data.crosschain.TradeBotData;
|
||||||
import org.qortal.data.network.TradePresenceData;
|
import org.qortal.data.network.TradePresenceData;
|
||||||
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.event.Event;
|
import org.qortal.event.Event;
|
||||||
import org.qortal.event.EventBus;
|
import org.qortal.event.EventBus;
|
||||||
import org.qortal.event.Listener;
|
import org.qortal.event.Listener;
|
||||||
@ -33,6 +35,7 @@ import org.qortal.repository.Repository;
|
|||||||
import org.qortal.repository.RepositoryManager;
|
import org.qortal.repository.RepositoryManager;
|
||||||
import org.qortal.repository.hsqldb.HSQLDBImportExport;
|
import org.qortal.repository.hsqldb.HSQLDBImportExport;
|
||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
|
import org.qortal.transaction.Transaction;
|
||||||
import org.qortal.utils.ByteArray;
|
import org.qortal.utils.ByteArray;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
@ -113,6 +116,9 @@ public class TradeBot implements Listener {
|
|||||||
private Map<ByteArray, TradePresenceData> safeAllTradePresencesByPubkey = Collections.emptyMap();
|
private Map<ByteArray, TradePresenceData> safeAllTradePresencesByPubkey = Collections.emptyMap();
|
||||||
private long nextTradePresenceBroadcastTimestamp = 0L;
|
private long nextTradePresenceBroadcastTimestamp = 0L;
|
||||||
|
|
||||||
|
private Map<String, Long> failedTrades = new HashMap<>();
|
||||||
|
private Map<String, Long> validTrades = new HashMap<>();
|
||||||
|
|
||||||
private TradeBot() {
|
private TradeBot() {
|
||||||
EventBus.INSTANCE.addListener(event -> TradeBot.getInstance().listen(event));
|
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) {
|
private long generateExpiry(long timestamp) {
|
||||||
return ((timestamp - 1) / EXPIRY_ROUNDING) * EXPIRY_ROUNDING + PRESENCE_LIFETIME;
|
return ((timestamp - 1) / EXPIRY_ROUNDING) * EXPIRY_ROUNDING + PRESENCE_LIFETIME;
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
|||||||
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorNode;
|
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorNode;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.voting.PollData;
|
import org.qortal.data.voting.PollData;
|
||||||
|
import org.qortal.data.voting.VoteOnPollData;
|
||||||
import org.qortal.transaction.Transaction.ApprovalStatus;
|
import org.qortal.transaction.Transaction.ApprovalStatus;
|
||||||
import org.qortal.transaction.Transaction.TransactionType;
|
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,
|
@XmlSeeAlso({GenesisTransactionData.class, PaymentTransactionData.class, RegisterNameTransactionData.class, UpdateNameTransactionData.class,
|
||||||
SellNameTransactionData.class, CancelSellNameTransactionData.class, BuyNameTransactionData.class,
|
SellNameTransactionData.class, CancelSellNameTransactionData.class, BuyNameTransactionData.class,
|
||||||
CreatePollTransactionData.class, VoteOnPollTransactionData.class, ArbitraryTransactionData.class,
|
CreatePollTransactionData.class, VoteOnPollTransactionData.class, ArbitraryTransactionData.class,
|
||||||
PollData.class,
|
PollData.class, VoteOnPollData.class,
|
||||||
IssueAssetTransactionData.class, TransferAssetTransactionData.class,
|
IssueAssetTransactionData.class, TransferAssetTransactionData.class,
|
||||||
CreateAssetOrderTransactionData.class, CancelAssetOrderTransactionData.class,
|
CreateAssetOrderTransactionData.class, CancelAssetOrderTransactionData.class,
|
||||||
MultiPaymentTransactionData.class, DeployAtTransactionData.class, MessageTransactionData.class, ATTransactionData.class,
|
MultiPaymentTransactionData.class, DeployAtTransactionData.class, MessageTransactionData.class, ATTransactionData.class,
|
||||||
|
@ -9,6 +9,11 @@ public class VoteOnPollData {
|
|||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
|
// For JAXB
|
||||||
|
protected VoteOnPollData() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
public VoteOnPollData(String pollName, byte[] voterPublicKey, int optionIndex) {
|
public VoteOnPollData(String pollName, byte[] voterPublicKey, int optionIndex) {
|
||||||
this.pollName = pollName;
|
this.pollName = pollName;
|
||||||
this.voterPublicKey = voterPublicKey;
|
this.voterPublicKey = voterPublicKey;
|
||||||
@ -21,12 +26,24 @@ public class VoteOnPollData {
|
|||||||
return this.pollName;
|
return this.pollName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPollName(String pollName) {
|
||||||
|
this.pollName = pollName;
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] getVoterPublicKey() {
|
public byte[] getVoterPublicKey() {
|
||||||
return this.voterPublicKey;
|
return this.voterPublicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setVoterPublicKey(byte[] voterPublicKey) {
|
||||||
|
this.voterPublicKey = voterPublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
public int getOptionIndex() {
|
public int getOptionIndex() {
|
||||||
return this.optionIndex;
|
return this.optionIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOptionIndex(int optionIndex) {
|
||||||
|
this.optionIndex = optionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,12 @@ public interface NameRepository {
|
|||||||
|
|
||||||
public boolean reducedNameExists(String reducedName) throws DataException;
|
public boolean reducedNameExists(String reducedName) throws DataException;
|
||||||
|
|
||||||
public List<NameData> searchNames(String query, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
public List<NameData> searchNames(String query, boolean prefixOnly, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||||
|
|
||||||
public List<NameData> getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException;
|
public List<NameData> getAllNames(Long after, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||||
|
|
||||||
public default List<NameData> getAllNames() throws DataException {
|
public default List<NameData> getAllNames() throws DataException {
|
||||||
return getAllNames(null, null, null);
|
return getAllNames(null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<NameData> getNamesForSale(Integer limit, Integer offset, Boolean reverse) throws DataException;
|
public List<NameData> getNamesForSale(Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||||
|
@ -103,7 +103,7 @@ public class HSQLDBNameRepository implements NameRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<NameData> searchNames(String query, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
public List<NameData> searchNames(String query, boolean prefixOnly, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||||
StringBuilder sql = new StringBuilder(512);
|
StringBuilder sql = new StringBuilder(512);
|
||||||
List<Object> bindParams = new ArrayList<>();
|
List<Object> bindParams = new ArrayList<>();
|
||||||
|
|
||||||
@ -111,7 +111,10 @@ public class HSQLDBNameRepository implements NameRepository {
|
|||||||
+ "is_for_sale, sale_price, reference, creation_group_id FROM Names "
|
+ "is_for_sale, sale_price, reference, creation_group_id FROM Names "
|
||||||
+ "WHERE LCASE(name) LIKE ? ORDER BY name");
|
+ "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)
|
if (reverse != null && reverse)
|
||||||
sql.append(" DESC");
|
sql.append(" DESC");
|
||||||
@ -155,11 +158,20 @@ public class HSQLDBNameRepository implements NameRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<NameData> getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException {
|
public List<NameData> getAllNames(Long after, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||||
StringBuilder sql = new StringBuilder(256);
|
StringBuilder sql = new StringBuilder(256);
|
||||||
|
List<Object> bindParams = new ArrayList<>();
|
||||||
|
|
||||||
sql.append("SELECT name, reduced_name, owner, data, registered_when, updated_when, "
|
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)
|
if (reverse != null && reverse)
|
||||||
sql.append(" DESC");
|
sql.append(" DESC");
|
||||||
@ -168,7 +180,7 @@ public class HSQLDBNameRepository implements NameRepository {
|
|||||||
|
|
||||||
List<NameData> names = new ArrayList<>();
|
List<NameData> names = new ArrayList<>();
|
||||||
|
|
||||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
|
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||||
if (resultSet == null)
|
if (resultSet == null)
|
||||||
return names;
|
return names;
|
||||||
|
|
||||||
|
@ -253,6 +253,9 @@ public class Settings {
|
|||||||
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
|
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
|
||||||
private boolean tradebotSystrayEnabled = false;
|
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 */
|
/** Wallets path - used for storing encrypted wallet caches for coins that require them */
|
||||||
private String walletsPath = "wallets";
|
private String walletsPath = "wallets";
|
||||||
|
|
||||||
@ -771,6 +774,10 @@ public class Settings {
|
|||||||
return this.pirateChainNet;
|
return this.pirateChainNet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getMaxTradeOfferAttempts() {
|
||||||
|
return this.maxTradeOfferAttempts;
|
||||||
|
}
|
||||||
|
|
||||||
public String getWalletsPath() {
|
public String getWalletsPath() {
|
||||||
return this.walletsPath;
|
return this.walletsPath;
|
||||||
}
|
}
|
||||||
|
@ -169,7 +169,7 @@ window.addEventListener("message", (event) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Core received event: " + JSON.stringify(event.data));
|
console.log("Core received action: " + JSON.stringify(event.data.action));
|
||||||
|
|
||||||
let url;
|
let url;
|
||||||
let data = event.data;
|
let data = event.data;
|
||||||
@ -181,6 +181,15 @@ window.addEventListener("message", (event) => {
|
|||||||
case "GET_ACCOUNT_NAMES":
|
case "GET_ACCOUNT_NAMES":
|
||||||
return httpGetAsyncWithEvent(event, "/names/address/" + data.address);
|
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":
|
case "GET_NAME_DATA":
|
||||||
return httpGetAsyncWithEvent(event, "/names/" + data.name);
|
return httpGetAsyncWithEvent(event, "/names/" + data.name);
|
||||||
|
|
||||||
|
@ -37,8 +37,8 @@ public class NamesApiTests extends ApiCommon {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetAllNames() {
|
public void testGetAllNames() {
|
||||||
assertNotNull(this.namesResource.getAllNames(null, null, null));
|
assertNotNull(this.namesResource.getAllNames(null, null, null, null));
|
||||||
assertNotNull(this.namesResource.getAllNames(1, 1, true));
|
assertNotNull(this.namesResource.getAllNames(1L, 1, 1, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
Loading…
x
Reference in New Issue
Block a user