mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-30 21:51:26 +00:00
Compare commits
49 Commits
v4.0.2
...
block-sequ
Author | SHA1 | Date | |
---|---|---|---|
|
ba555174ba | ||
|
3763035d4a | ||
|
b1a904a3c7 | ||
|
3c4c5a1457 | ||
|
648fa66f6a | ||
|
072aa469e3 | ||
|
2b2d6f4e52 | ||
|
c6456669e2 | ||
|
a74fa15d60 | ||
|
68b99c8643 | ||
|
b9015217de | ||
|
e1043ceacb | ||
|
8b51590844 | ||
|
a8d92805f9 | ||
|
2cc5b90306 | ||
|
4cb755a2f1 | ||
|
92119b5558 | ||
|
8a1bf8b5ec | ||
|
f8233bd05b | ||
|
29480e5664 | ||
|
5a873f9465 | ||
|
dc1289787d | ||
|
ba4866a2e6 | ||
|
2cbc5aabd5 | ||
|
e3be43a1e6 | ||
|
a575ea4423 | ||
|
3e45948646 | ||
|
49c0d45bc6 | ||
|
cda32a47f1 | ||
|
49063e54ec | ||
|
df3c68679f | ||
|
81788610c4 | ||
|
fc10b61193 | ||
|
05b4ecd4ed | ||
|
aba589c0e0 | ||
|
c682fa89fd | ||
|
21d1750779 | ||
|
923e90ebed | ||
|
9490c62242 | ||
|
c941bc6024 | ||
|
a4551245cb | ||
|
e4f45c1a70 | ||
|
bc44b998dc | ||
|
b89a35ac69 | ||
|
9566bda279 | ||
|
20d4e88fab | ||
|
a8c27be18a | ||
|
af6be759e7 | ||
|
896d814385 |
16
Q-Apps.md
16
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({
|
||||
@@ -425,7 +438,8 @@ let res = await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_STATUS",
|
||||
name: "QortalDemo",
|
||||
service: "THUMBNAIL",
|
||||
identifier: "qortal_avatar" // Optional
|
||||
identifier: "qortal_avatar", // Optional
|
||||
build: true // Optional - request that the resource is fetched & built in the background
|
||||
});
|
||||
```
|
||||
|
||||
|
2
pom.xml
2
pom.xml
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>4.0.2</version>
|
||||
<version>4.0.3</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1267,7 +1267,8 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
|
||||
} catch (DataException | IOException e) {
|
||||
} catch (Exception e) {
|
||||
LOGGER.info("Exception when publishing data: ", e);
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -1315,7 +1316,7 @@ public class ArbitraryResource {
|
||||
if (filepath == null || filepath.isEmpty()) {
|
||||
// No file path supplied - so check if this is a single file resource
|
||||
String[] files = ArrayUtils.removeElement(outputPath.toFile().list(), ".qortal");
|
||||
if (files.length == 1) {
|
||||
if (files != null && files.length == 1) {
|
||||
// This is a single file resource
|
||||
filepath = files[0];
|
||||
}
|
||||
|
@@ -222,14 +222,25 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Check if the block exists in either the database or archive
|
||||
if (repository.getBlockRepository().getHeightFromSignature(signature) == 0 &&
|
||||
repository.getBlockArchiveRepository().getHeightFromSignature(signature) == 0) {
|
||||
// Not found in either the database or archive
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
// Check if the block exists in either the database or archive
|
||||
int height = repository.getBlockRepository().getHeightFromSignature(signature);
|
||||
if (height == 0) {
|
||||
height = repository.getBlockArchiveRepository().getHeightFromSignature(signature);
|
||||
if (height == 0) {
|
||||
// Not found in either the database or archive
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse);
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, height, height);
|
||||
|
||||
// Expand signatures to transactions
|
||||
List<TransactionData> transactions = new ArrayList<>(signatures.size());
|
||||
for (byte[] s : signatures) {
|
||||
transactions.add(repository.getTransactionRepository().fromSignature(s));
|
||||
}
|
||||
|
||||
return transactions;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
@@ -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());
|
||||
@@ -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
|
||||
@Path("/trade/{ataddress}")
|
||||
@Operation(
|
||||
|
@@ -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")
|
||||
@@ -63,19 +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<NameSummary> getAllNames(@Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref="reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
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) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<NameData> names = repository.getNameRepository().getAllNames(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);
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -171,6 +173,7 @@ public class NamesResource {
|
||||
)
|
||||
@ApiErrors({ApiError.NAME_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
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 = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref="reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
@@ -179,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) {
|
||||
@@ -442,4 +447,4 @@ public class NamesResource {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -31,12 +31,18 @@ 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")
|
||||
@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
|
||||
@Path("/create")
|
||||
@Operation(
|
||||
|
70
src/main/java/org/qortal/api/resource/StatsResource.java
Normal file
70
src/main/java/org/qortal/api/resource/StatsResource.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.api.*;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.utils.Amounts;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Path("/stats")
|
||||
@Tag(name = "Stats")
|
||||
public class StatsResource {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(StatsResource.class);
|
||||
|
||||
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
|
||||
@GET
|
||||
@Path("/supply/circulating")
|
||||
@Operation(
|
||||
summary = "Fetch circulating QORT supply",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "circulating supply of QORT",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", format = "number"))
|
||||
)
|
||||
}
|
||||
)
|
||||
public BigDecimal circulatingSupply() {
|
||||
long total = 0L;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int currentHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
List<BlockChain.RewardByHeight> rewardsByHeight = BlockChain.getInstance().getBlockRewardsByHeight();
|
||||
int rewardIndex = rewardsByHeight.size() - 1;
|
||||
BlockChain.RewardByHeight rewardInfo = rewardsByHeight.get(rewardIndex);
|
||||
|
||||
for (int height = currentHeight; height > 1; --height) {
|
||||
if (height < rewardInfo.height) {
|
||||
--rewardIndex;
|
||||
rewardInfo = rewardsByHeight.get(rewardIndex);
|
||||
}
|
||||
|
||||
total += rewardInfo.reward;
|
||||
}
|
||||
|
||||
return Amounts.toBigDecimal(total);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -215,10 +215,25 @@ public class TransactionsResource {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
if (repository.getBlockRepository().getHeightFromSignature(signature) == 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
// Check if the block exists in either the database or archive
|
||||
int height = repository.getBlockRepository().getHeightFromSignature(signature);
|
||||
if (height == 0) {
|
||||
height = repository.getBlockArchiveRepository().getHeightFromSignature(signature);
|
||||
if (height == 0) {
|
||||
// Not found in either the database or archive
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse);
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, height, height);
|
||||
|
||||
// Expand signatures to transactions
|
||||
List<TransactionData> transactions = new ArrayList<>(signatures.size());
|
||||
for (byte[] s : signatures) {
|
||||
transactions.add(repository.getTransactionRepository().fromSignature(s));
|
||||
}
|
||||
|
||||
return transactions;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -34,6 +34,9 @@ import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ArbitraryDataReader {
|
||||
|
||||
@@ -59,6 +62,10 @@ public class ArbitraryDataReader {
|
||||
// The resource being read
|
||||
ArbitraryDataResource arbitraryDataResource = null;
|
||||
|
||||
// Track resources that are currently being loaded, to avoid duplicate concurrent builds
|
||||
// TODO: all builds could be handled by the build queue (even synchronous ones), to avoid the need for this
|
||||
private static Map<String, Long> inProgress = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
public ArbitraryDataReader(String resourceId, ResourceIdType resourceIdType, Service service, String identifier) {
|
||||
// Ensure names are always lowercase
|
||||
if (resourceIdType == ResourceIdType.NAME) {
|
||||
@@ -166,6 +173,12 @@ public class ArbitraryDataReader {
|
||||
|
||||
this.arbitraryDataResource = this.createArbitraryDataResource();
|
||||
|
||||
// Don't allow duplicate loads
|
||||
if (!this.canStartLoading()) {
|
||||
LOGGER.debug("Skipping duplicate load of {}", this.arbitraryDataResource);
|
||||
return;
|
||||
}
|
||||
|
||||
this.preExecute();
|
||||
this.deleteExistingFiles();
|
||||
this.fetch();
|
||||
@@ -193,6 +206,7 @@ public class ArbitraryDataReader {
|
||||
|
||||
private void preExecute() throws DataException {
|
||||
ArbitraryDataBuildManager.getInstance().setBuildInProgress(true);
|
||||
|
||||
this.checkEnabled();
|
||||
this.createWorkingDirectory();
|
||||
this.createUncompressedDirectory();
|
||||
@@ -200,6 +214,9 @@ public class ArbitraryDataReader {
|
||||
|
||||
private void postExecute() {
|
||||
ArbitraryDataBuildManager.getInstance().setBuildInProgress(false);
|
||||
|
||||
this.arbitraryDataResource = this.createArbitraryDataResource();
|
||||
ArbitraryDataReader.inProgress.remove(this.arbitraryDataResource.getUniqueKey());
|
||||
}
|
||||
|
||||
private void checkEnabled() throws DataException {
|
||||
@@ -208,6 +225,17 @@ public class ArbitraryDataReader {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canStartLoading() {
|
||||
// Avoid duplicate builds if we're already loading this resource
|
||||
String uniqueKey = this.arbitraryDataResource.getUniqueKey();
|
||||
if (ArbitraryDataReader.inProgress.containsKey(uniqueKey)) {
|
||||
return false;
|
||||
}
|
||||
ArbitraryDataReader.inProgress.put(uniqueKey, NTP.getTime());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void createWorkingDirectory() throws DataException {
|
||||
try {
|
||||
Files.createDirectories(this.workingPath);
|
||||
@@ -441,6 +469,7 @@ public class ArbitraryDataReader {
|
||||
Path unencryptedPath = Paths.get(this.workingPath.toString(), "zipped.zip");
|
||||
SecretKey aesKey = new SecretKeySpec(secret, 0, secret.length, "AES");
|
||||
AES.decryptFile(algorithm, aesKey, this.filePath.toString(), unencryptedPath.toString());
|
||||
LOGGER.debug("Finished decrypting {} using algorithm {}", this.arbitraryDataResource, algorithm);
|
||||
|
||||
// Replace filePath pointer with the encrypted file path
|
||||
// Don't delete the original ArbitraryDataFile, as this is handled in the cleanup phase
|
||||
@@ -475,7 +504,9 @@ public class ArbitraryDataReader {
|
||||
|
||||
// Handle each type of compression
|
||||
if (compression == Compression.ZIP) {
|
||||
LOGGER.debug("Unzipping {}...", this.arbitraryDataResource);
|
||||
ZipUtils.unzip(this.filePath.toString(), this.uncompressedPath.getParent().toString());
|
||||
LOGGER.debug("Finished unzipping {}", this.arbitraryDataResource);
|
||||
}
|
||||
else if (compression == Compression.NONE) {
|
||||
Files.createDirectories(this.uncompressedPath);
|
||||
@@ -511,10 +542,12 @@ public class ArbitraryDataReader {
|
||||
|
||||
private void validate() throws IOException, DataException {
|
||||
if (this.service.isValidationRequired()) {
|
||||
LOGGER.debug("Validating {}...", this.arbitraryDataResource);
|
||||
Service.ValidationResult result = this.service.validate(this.filePath);
|
||||
if (result != Service.ValidationResult.OK) {
|
||||
throw new DataException(String.format("Validation of %s failed: %s", this.service, result.toString()));
|
||||
}
|
||||
LOGGER.debug("Finished validating {}", this.arbitraryDataResource);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -107,7 +107,7 @@ public enum Service {
|
||||
}
|
||||
|
||||
// Require valid JSON
|
||||
byte[] data = FilesystemUtils.getSingleFileContents(path);
|
||||
byte[] data = FilesystemUtils.getSingleFileContents(path, 25*1024);
|
||||
String json = new String(data, StandardCharsets.UTF_8);
|
||||
try {
|
||||
objectMapper.readTree(json);
|
||||
@@ -201,7 +201,9 @@ public enum Service {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
byte[] data = FilesystemUtils.getSingleFileContents(path);
|
||||
// Load the first 25KB of data. This only needs to be long enough to check the prefix
|
||||
// and also to allow for possible additional future validation of smaller files.
|
||||
byte[] data = FilesystemUtils.getSingleFileContents(path, 25*1024);
|
||||
long size = FilesystemUtils.getDirectorySize(path);
|
||||
|
||||
// Validate max size if needed
|
||||
|
@@ -1686,12 +1686,14 @@ public class Block {
|
||||
transactionData.getSignature());
|
||||
this.repository.getBlockRepository().save(blockTransactionData);
|
||||
|
||||
// Update transaction's height in repository
|
||||
// Update transaction's height in repository and local transactionData
|
||||
transactionRepository.updateBlockHeight(transactionData.getSignature(), this.blockData.getHeight());
|
||||
|
||||
// Update local transactionData's height too
|
||||
transaction.getTransactionData().setBlockHeight(this.blockData.getHeight());
|
||||
|
||||
// Update transaction's sequence in repository and local transactionData
|
||||
transactionRepository.updateBlockSequence(transactionData.getSignature(), sequence);
|
||||
transaction.getTransactionData().setBlockSequence(sequence);
|
||||
|
||||
// No longer unconfirmed
|
||||
transactionRepository.confirmTransaction(transactionData.getSignature());
|
||||
|
||||
@@ -1778,6 +1780,9 @@ public class Block {
|
||||
|
||||
// Unset height
|
||||
transactionRepository.updateBlockHeight(transactionData.getSignature(), null);
|
||||
|
||||
// Unset sequence
|
||||
transactionRepository.updateBlockSequence(transactionData.getSignature(), null);
|
||||
}
|
||||
|
||||
transactionRepository.deleteParticipants(transactionData);
|
||||
|
@@ -871,6 +871,9 @@ public class BlockChain {
|
||||
BlockData orphanBlockData = repository.getBlockRepository().fromHeight(height);
|
||||
|
||||
while (height > targetHeight) {
|
||||
if (Controller.isStopping()) {
|
||||
return false;
|
||||
}
|
||||
LOGGER.info(String.format("Forcably orphaning block %d", height));
|
||||
|
||||
Block block = new Block(repository, orphanBlockData);
|
||||
|
@@ -400,10 +400,13 @@ public class Controller extends Thread {
|
||||
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(getRepositoryUrl());
|
||||
RepositoryManager.setRepositoryFactory(repositoryFactory);
|
||||
RepositoryManager.setRequestedCheckpoint(Boolean.TRUE);
|
||||
}
|
||||
catch (DataException e) {
|
||||
// If exception has no cause then repository is in use by some other process.
|
||||
if (e.getCause() == null) {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
RepositoryManager.rebuildTransactionSequences(repository);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
// If exception has no cause or message then repository is in use by some other process.
|
||||
if (e.getCause() == null && e.getMessage() == null) {
|
||||
LOGGER.info("Repository in use by another process?");
|
||||
Gui.getInstance().fatalError("Repository issue", "Repository in use by another process?");
|
||||
} else {
|
||||
@@ -437,6 +440,18 @@ public class Controller extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
try (Repository repository = RepositoryManager.getRepository()) {
|
||||
if (RepositoryManager.needsTransactionSequenceRebuild(repository)) {
|
||||
// Don't allow the node to start if transaction sequences haven't been built yet
|
||||
// This is needed to handle a case when bootstrapping
|
||||
Gui.getInstance().fatalError("Database upgrade needed", "Please restart the core to complete the upgrade process.");
|
||||
return;
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Error checking transaction sequences in repository", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Import current trade bot states and minting accounts if they exist
|
||||
Controller.importRepositoryData();
|
||||
|
||||
|
@@ -57,6 +57,8 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
* This must be higher than STORAGE_FULL_THRESHOLD in order to avoid a fetch/delete loop. */
|
||||
public static final double DELETION_THRESHOLD = 0.98f; // 98%
|
||||
|
||||
private static final long PER_NAME_STORAGE_MULTIPLIER = 4L;
|
||||
|
||||
public ArbitraryDataStorageManager() {
|
||||
}
|
||||
|
||||
@@ -488,6 +490,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;
|
||||
@@ -530,7 +537,9 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
}
|
||||
|
||||
double maxStorageCapacity = (double)this.storageCapacity * threshold;
|
||||
long maxStoragePerName = (long)(maxStorageCapacity / (double)followedNamesCount);
|
||||
|
||||
// Some names won't need/use much space, so give all names a 4x multiplier to compensate
|
||||
long maxStoragePerName = (long)(maxStorageCapacity / (double)followedNamesCount) * PER_NAME_STORAGE_MULTIPLIER;
|
||||
|
||||
return maxStoragePerName;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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,
|
||||
@@ -78,6 +79,10 @@ public abstract class TransactionData {
|
||||
@Schema(accessMode = AccessMode.READ_ONLY, hidden = true, description = "height of block containing transaction")
|
||||
protected Integer blockHeight;
|
||||
|
||||
// Not always present
|
||||
@Schema(accessMode = AccessMode.READ_ONLY, hidden = true, description = "sequence in block containing transaction")
|
||||
protected Integer blockSequence;
|
||||
|
||||
// Not always present
|
||||
@Schema(accessMode = AccessMode.READ_ONLY, description = "group-approval status")
|
||||
protected ApprovalStatus approvalStatus;
|
||||
@@ -108,6 +113,7 @@ public abstract class TransactionData {
|
||||
this.fee = baseTransactionData.fee;
|
||||
this.signature = baseTransactionData.signature;
|
||||
this.blockHeight = baseTransactionData.blockHeight;
|
||||
this.blockSequence = baseTransactionData.blockSequence;
|
||||
this.approvalStatus = baseTransactionData.approvalStatus;
|
||||
this.approvalHeight = baseTransactionData.approvalHeight;
|
||||
}
|
||||
@@ -176,6 +182,15 @@ public abstract class TransactionData {
|
||||
this.blockHeight = blockHeight;
|
||||
}
|
||||
|
||||
public Integer getBlockSequence() {
|
||||
return this.blockSequence;
|
||||
}
|
||||
|
||||
@XmlTransient
|
||||
public void setBlockSequence(Integer blockSequence) {
|
||||
this.blockSequence = blockSequence;
|
||||
}
|
||||
|
||||
public ApprovalStatus getApprovalStatus() {
|
||||
return approvalStatus;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -265,7 +265,7 @@ public enum Handshake {
|
||||
private static final long PEER_VERSION_131 = 0x0100030001L;
|
||||
|
||||
/** Minimum peer version that we are allowed to communicate with */
|
||||
private static final String MIN_PEER_VERSION = "3.8.2";
|
||||
private static final String MIN_PEER_VERSION = "4.0.0";
|
||||
|
||||
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
|
||||
private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits
|
||||
|
@@ -14,12 +14,12 @@ public interface NameRepository {
|
||||
|
||||
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 {
|
||||
return getAllNames(null, null, null);
|
||||
return getAllNames(null, null, null, null);
|
||||
}
|
||||
|
||||
public List<NameData> getNamesForSale(Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
@@ -2,9 +2,23 @@ package org.qortal.repository;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.ATTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.gui.SplashFrame;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transform.block.BlockTransformation;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.qortal.transaction.Transaction.TransactionType.AT;
|
||||
|
||||
public abstract class RepositoryManager {
|
||||
private static final Logger LOGGER = LogManager.getLogger(RepositoryManager.class);
|
||||
@@ -56,6 +70,164 @@ public abstract class RepositoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean needsTransactionSequenceRebuild(Repository repository) throws DataException {
|
||||
// Check if we have any unpopulated block_sequence values for the first 1000 blocks
|
||||
List<byte[]> testSignatures = repository.getTransactionRepository().getSignaturesMatchingCustomCriteria(
|
||||
null, Arrays.asList("block_height < 1000 AND block_sequence IS NULL"), new ArrayList<>());
|
||||
if (testSignatures.isEmpty()) {
|
||||
// block_sequence already populated for the first 1000 blocks, so assume complete.
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean rebuildTransactionSequences(Repository repository) throws DataException {
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes have no blockchain
|
||||
return false;
|
||||
}
|
||||
if (Settings.getInstance().isTopOnly()) {
|
||||
// topOnly nodes are unable to perform this reindex, and so are temporarily unsupported
|
||||
throw new DataException("topOnly nodes are now unsupported, as they are missing data required for a db reshape");
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if we have any unpopulated block_sequence values for the first 1000 blocks
|
||||
if (!needsTransactionSequenceRebuild(repository)) {
|
||||
// block_sequence already populated for the first 1000 blocks, so assume complete.
|
||||
// We avoid checkpointing and prevent the node from starting up in the case of a rebuild failure, so
|
||||
// we shouldn't ever be left in a partially rebuilt state.
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.info("Rebuilding transaction sequences - this will take a while...");
|
||||
|
||||
SplashFrame.getInstance().updateStatus("Rebuilding transactions - please wait...");
|
||||
|
||||
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
int totalTransactionCount = 0;
|
||||
|
||||
for (int height = 1; height < blockchainHeight; ++height) {
|
||||
List<TransactionData> inputTransactions = new ArrayList<>();
|
||||
|
||||
// Fetch block and transactions
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
boolean loadedFromArchive = false;
|
||||
if (blockData == null) {
|
||||
// Get (non-AT) transactions from the archive
|
||||
BlockTransformation blockTransformation = BlockArchiveReader.getInstance().fetchBlockAtHeight(height);
|
||||
blockData = blockTransformation.getBlockData();
|
||||
inputTransactions = blockTransformation.getTransactions(); // This doesn't include AT transactions
|
||||
loadedFromArchive = true;
|
||||
}
|
||||
else {
|
||||
// Get transactions from db
|
||||
Block block = new Block(repository, blockData);
|
||||
for (Transaction transaction : block.getTransactions()) {
|
||||
inputTransactions.add(transaction.getTransactionData());
|
||||
}
|
||||
}
|
||||
|
||||
if (blockData == null) {
|
||||
throw new DataException("Missing block data");
|
||||
}
|
||||
|
||||
List<TransactionData> transactions = new ArrayList<>();
|
||||
|
||||
if (loadedFromArchive) {
|
||||
List<TransactionData> transactionDataList = new ArrayList<>(blockData.getTransactionCount());
|
||||
// Fetch any AT transactions in this block
|
||||
List<byte[]> atSignatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, height, height);
|
||||
for (byte[] s : atSignatures) {
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(s);
|
||||
if (transactionData.getType() == AT) {
|
||||
transactionDataList.add(transactionData);
|
||||
}
|
||||
}
|
||||
|
||||
List<ATTransactionData> atTransactions = new ArrayList<>();
|
||||
for (TransactionData transactionData : transactionDataList) {
|
||||
ATTransactionData atTransactionData = (ATTransactionData) transactionData;
|
||||
atTransactions.add(atTransactionData);
|
||||
}
|
||||
|
||||
// Create sorted list of ATs by creation time
|
||||
List<ATData> ats = new ArrayList<>();
|
||||
|
||||
for (ATTransactionData atTransactionData : atTransactions) {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atTransactionData.getATAddress());
|
||||
boolean hasExistingEntry = ats.stream().anyMatch(a -> Objects.equals(a.getATAddress(), atTransactionData.getATAddress()));
|
||||
if (!hasExistingEntry) {
|
||||
ats.add(atData);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort list of ATs by creation date
|
||||
ats.sort(Comparator.comparingLong(ATData::getCreation));
|
||||
|
||||
// Loop through unique ATs
|
||||
for (ATData atData : ats) {
|
||||
List<ATTransactionData> thisAtTransactions = atTransactions.stream()
|
||||
.filter(t -> Objects.equals(t.getATAddress(), atData.getATAddress()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
int count = thisAtTransactions.size();
|
||||
|
||||
if (count == 1) {
|
||||
ATTransactionData atTransactionData = thisAtTransactions.get(0);
|
||||
transactions.add(atTransactionData);
|
||||
}
|
||||
else if (count == 2) {
|
||||
String atCreatorAddress = Crypto.toAddress(atData.getCreatorPublicKey());
|
||||
|
||||
ATTransactionData atTransactionData1 = thisAtTransactions.stream()
|
||||
.filter(t -> !Objects.equals(t.getRecipient(), atCreatorAddress))
|
||||
.findFirst().orElse(null);
|
||||
transactions.add(atTransactionData1);
|
||||
|
||||
ATTransactionData atTransactionData2 = thisAtTransactions.stream()
|
||||
.filter(t -> Objects.equals(t.getRecipient(), atCreatorAddress))
|
||||
.findFirst().orElse(null);
|
||||
transactions.add(atTransactionData2);
|
||||
}
|
||||
else if (count > 2) {
|
||||
LOGGER.info("Error: AT has more than 2 output transactions");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add all the regular transactions now that AT transactions have been handled
|
||||
transactions.addAll(inputTransactions);
|
||||
totalTransactionCount += transactions.size();
|
||||
|
||||
// Loop through and update sequences
|
||||
for (int sequence = 0; sequence < transactions.size(); ++sequence) {
|
||||
TransactionData transactionData = transactions.get(sequence);
|
||||
|
||||
// Update transaction's sequence in repository
|
||||
repository.getTransactionRepository().updateBlockSequence(transactionData.getSignature(), sequence);
|
||||
}
|
||||
|
||||
if (height % 10000 == 0) {
|
||||
LOGGER.info("Rebuilt sequences for {} blocks (total transactions: {})", height, totalTransactionCount);
|
||||
}
|
||||
|
||||
repository.saveChanges();
|
||||
}
|
||||
|
||||
LOGGER.info("Completed rebuild of transaction sequences.");
|
||||
return true;
|
||||
}
|
||||
catch (DataException e) {
|
||||
LOGGER.info("Unable to rebuild transaction sequences: {}. The database may have been left in an inconsistent state.", e.getMessage());
|
||||
|
||||
// Throw an exception so that the node startup is halted, allowing for a retry next time.
|
||||
repository.discardChanges();
|
||||
throw new DataException("Rebuild of transaction sequences failed.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void setRequestedCheckpoint(Boolean quick) {
|
||||
quickCheckpointRequested = quick;
|
||||
}
|
||||
|
@@ -309,6 +309,8 @@ public interface TransactionRepository {
|
||||
|
||||
public void updateBlockHeight(byte[] signature, Integer height) throws DataException;
|
||||
|
||||
public void updateBlockSequence(byte[] signature, Integer sequence) throws DataException;
|
||||
|
||||
public void updateApprovalHeight(byte[] signature, Integer approvalHeight) throws DataException;
|
||||
|
||||
/**
|
||||
|
@@ -296,10 +296,9 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
@Override
|
||||
public Integer getATCreationBlockHeight(String atAddress) throws DataException {
|
||||
String sql = "SELECT height "
|
||||
String sql = "SELECT block_height "
|
||||
+ "FROM DeployATTransactions "
|
||||
+ "JOIN BlockTransactions ON transaction_signature = signature "
|
||||
+ "JOIN Blocks ON Blocks.signature = block_signature "
|
||||
+ "JOIN Transactions USING (signature) "
|
||||
+ "WHERE AT_address = ? "
|
||||
+ "LIMIT 1";
|
||||
|
||||
@@ -877,18 +876,17 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
public NextTransactionInfo findNextTransaction(String recipient, int height, int sequence) throws DataException {
|
||||
// We only need to search for a subset of transaction types: MESSAGE, PAYMENT or AT
|
||||
|
||||
String sql = "SELECT height, sequence, Transactions.signature "
|
||||
String sql = "SELECT block_height, block_sequence, Transactions.signature "
|
||||
+ "FROM ("
|
||||
+ "SELECT signature FROM PaymentTransactions WHERE recipient = ? "
|
||||
+ "UNION "
|
||||
+ "SELECT signature FROM MessageTransactions WHERE recipient = ? "
|
||||
+ "UNION "
|
||||
+ "SELECT signature FROM ATTransactions WHERE recipient = ?"
|
||||
+ ") AS Transactions "
|
||||
+ "JOIN BlockTransactions ON BlockTransactions.transaction_signature = Transactions.signature "
|
||||
+ "JOIN Blocks ON Blocks.signature = BlockTransactions.block_signature "
|
||||
+ "WHERE (height > ? OR (height = ? AND sequence > ?)) "
|
||||
+ "ORDER BY height ASC, sequence ASC "
|
||||
+ ") AS SelectedTransactions "
|
||||
+ "JOIN Transactions USING (signature)"
|
||||
+ "WHERE (block_height > ? OR (block_height = ? AND block_sequence > ?)) "
|
||||
+ "ORDER BY block_height ASC, block_sequence ASC "
|
||||
+ "LIMIT 1";
|
||||
|
||||
Object[] bindParams = new Object[] { recipient, recipient, recipient, height, height, sequence };
|
||||
|
@@ -993,6 +993,17 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("ALTER TABLE CancelSellNameTransactions ADD sale_price QortalAmount");
|
||||
break;
|
||||
|
||||
case 47:
|
||||
// Add `block_sequence` to the Transaction table, as the BlockTransactions table is pruned for
|
||||
// older blocks and therefore the sequence becomes unavailable
|
||||
LOGGER.info("Reshaping Transactions table - this can take a while...");
|
||||
stmt.execute("ALTER TABLE Transactions ADD block_sequence INTEGER");
|
||||
|
||||
// For finding transactions by height and sequence
|
||||
LOGGER.info("Adding index to Transactions table - this can take a while...");
|
||||
stmt.execute("CREATE INDEX TransactionHeightSequenceIndex on Transactions (block_height, block_sequence)");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@@ -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);
|
||||
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 "
|
||||
+ "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");
|
||||
@@ -155,11 +158,20 @@ public class HSQLDBNameRepository implements NameRepository {
|
||||
}
|
||||
|
||||
@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);
|
||||
List<Object> 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");
|
||||
@@ -168,7 +180,7 @@ public class HSQLDBNameRepository implements NameRepository {
|
||||
|
||||
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)
|
||||
return names;
|
||||
|
||||
|
@@ -194,8 +194,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
|
||||
@Override
|
||||
public TransactionData fromHeightAndSequence(int height, int sequence) throws DataException {
|
||||
String sql = "SELECT transaction_signature FROM BlockTransactions JOIN Blocks ON signature = block_signature "
|
||||
+ "WHERE height = ? AND sequence = ?";
|
||||
String sql = "SELECT signature FROM Transactions WHERE block_height = ? AND block_sequence = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, height, sequence)) {
|
||||
if (resultSet == null)
|
||||
@@ -657,8 +656,13 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
List<Object> bindParams) throws DataException {
|
||||
List<byte[]> signatures = new ArrayList<>();
|
||||
|
||||
String txTypeClassName = "";
|
||||
if (txType != null) {
|
||||
txTypeClassName = txType.className;
|
||||
}
|
||||
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append(String.format("SELECT signature FROM %sTransactions", txType.className));
|
||||
sql.append(String.format("SELECT signature FROM %sTransactions", txTypeClassName));
|
||||
|
||||
if (!whereClauses.isEmpty()) {
|
||||
sql.append(" WHERE ");
|
||||
@@ -1444,6 +1448,19 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBlockSequence(byte[] signature, Integer blockSequence) throws DataException {
|
||||
HSQLDBSaver saver = new HSQLDBSaver("Transactions");
|
||||
|
||||
saver.bind("signature", signature).bind("block_sequence", blockSequence);
|
||||
|
||||
try {
|
||||
saver.execute(repository);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to update transaction's block sequence in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateApprovalHeight(byte[] signature, Integer approvalHeight) throws DataException {
|
||||
HSQLDBSaver saver = new HSQLDBSaver("Transactions");
|
||||
|
@@ -201,11 +201,11 @@ public class Settings {
|
||||
/** Whether to attempt to open the listen port via UPnP */
|
||||
private boolean uPnPEnabled = true;
|
||||
/** Minimum number of peers to allow block minting / synchronization. */
|
||||
private int minBlockchainPeers = 5;
|
||||
private int minBlockchainPeers = 3;
|
||||
/** Target number of outbound connections to peers we should make. */
|
||||
private int minOutboundPeers = 16;
|
||||
/** Maximum number of peer connections we allow. */
|
||||
private int maxPeers = 36;
|
||||
private int maxPeers = 40;
|
||||
/** Number of slots to reserve for short-lived QDN data transfers */
|
||||
private int maxDataPeers = 4;
|
||||
/** Maximum number of threads for network engine. */
|
||||
@@ -216,10 +216,10 @@ public class Settings {
|
||||
private int maxRetries = 2;
|
||||
|
||||
/** The number of seconds of no activity before recovery mode begins */
|
||||
public long recoveryModeTimeout = 10 * 60 * 1000L;
|
||||
public long recoveryModeTimeout = 24 * 60 * 60 * 1000L;
|
||||
|
||||
/** Minimum peer version number required in order to sync with them */
|
||||
private String minPeerVersion = "3.8.7";
|
||||
private String minPeerVersion = "4.0.0";
|
||||
/** Whether to allow connections with peers below minPeerVersion
|
||||
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
|
||||
* If false, sync will be blocked both ways, and they will not appear in the peers list */
|
||||
@@ -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";
|
||||
|
||||
@@ -505,6 +508,9 @@ public class Settings {
|
||||
if (this.minBlockchainPeers < 1 && !singleNodeTestnet)
|
||||
throwValidationError("minBlockchainPeers must be at least 1");
|
||||
|
||||
if (this.topOnly)
|
||||
throwValidationError("topOnly mode is no longer supported");
|
||||
|
||||
if (this.apiKey != null && this.apiKey.trim().length() < 8)
|
||||
throwValidationError("apiKey must be at least 8 characters");
|
||||
|
||||
@@ -771,6 +777,10 @@ public class Settings {
|
||||
return this.pirateChainNet;
|
||||
}
|
||||
|
||||
public int getMaxTradeOfferAttempts() {
|
||||
return this.maxTradeOfferAttempts;
|
||||
}
|
||||
|
||||
public String getWalletsPath() {
|
||||
return this.walletsPath;
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package org.qortal.utils;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -12,6 +14,8 @@ import java.util.List;
|
||||
|
||||
public class BlockArchiveUtils {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockArchiveUtils.class);
|
||||
|
||||
/**
|
||||
* importFromArchive
|
||||
* <p>
|
||||
@@ -87,7 +91,8 @@ public class BlockArchiveUtils {
|
||||
|
||||
} catch (DataException e) {
|
||||
repository.discardChanges();
|
||||
throw new IllegalStateException("Unable to import blocks from archive");
|
||||
LOGGER.info("Unable to import blocks from archive", e);
|
||||
throw(e);
|
||||
}
|
||||
}
|
||||
repository.saveChanges();
|
||||
|
@@ -228,12 +228,18 @@ public class FilesystemUtils {
|
||||
* @throws IOException
|
||||
*/
|
||||
public static byte[] getSingleFileContents(Path path) throws IOException {
|
||||
return getSingleFileContents(path, null);
|
||||
}
|
||||
|
||||
public static byte[] getSingleFileContents(Path path, Integer maxLength) throws IOException {
|
||||
byte[] data = null;
|
||||
// TODO: limit the file size that can be loaded into memory
|
||||
|
||||
// If the path is a file, read the contents directly
|
||||
if (path.toFile().isFile()) {
|
||||
data = Files.readAllBytes(path);
|
||||
int fileSize = (int)path.toFile().length();
|
||||
maxLength = maxLength != null ? Math.min(maxLength, fileSize) : fileSize;
|
||||
data = FilesystemUtils.readFromFile(path.toString(), 0, maxLength);
|
||||
}
|
||||
|
||||
// Or if it's a directory, only load file contents if there is a single file inside it
|
||||
@@ -242,7 +248,9 @@ public class FilesystemUtils {
|
||||
if (files.length == 1) {
|
||||
Path filePath = Paths.get(path.toString(), files[0]);
|
||||
if (filePath.toFile().isFile()) {
|
||||
data = Files.readAllBytes(filePath);
|
||||
int fileSize = (int)filePath.toFile().length();
|
||||
maxLength = maxLength != null ? Math.min(maxLength, fileSize) : fileSize;
|
||||
data = FilesystemUtils.readFromFile(filePath.toString(), 0, maxLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -236,13 +245,15 @@ window.addEventListener("message", (event) => {
|
||||
if (data.identifier != null) url = url.concat("/" + data.identifier);
|
||||
url = url.concat("?");
|
||||
if (data.filepath != null) url = url.concat("&filepath=" + data.filepath);
|
||||
if (data.rebuild != null) url = url.concat("&rebuild=" + new Boolean(data.rebuild).toString())
|
||||
if (data.rebuild != null) url = url.concat("&rebuild=" + new Boolean(data.rebuild).toString());
|
||||
if (data.encoding != null) url = url.concat("&encoding=" + data.encoding);
|
||||
return httpGetAsyncWithEvent(event, url);
|
||||
|
||||
case "GET_QDN_RESOURCE_STATUS":
|
||||
url = "/arbitrary/resource/status/" + data.service + "/" + data.name;
|
||||
if (data.identifier != null) url = url.concat("/" + data.identifier);
|
||||
url = url.concat("?");
|
||||
if (data.build != null) url = url.concat("&build=" + new Boolean(data.build).toString());
|
||||
return httpGetAsyncWithEvent(event, url);
|
||||
|
||||
case "GET_QDN_RESOURCE_PROPERTIES":
|
||||
|
@@ -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
|
||||
|
@@ -113,13 +113,16 @@ public class ArbitraryDataStorageCapacityTests extends Common {
|
||||
assertTrue(resourceListManager.addToList("followedNames", "Test2", false));
|
||||
assertTrue(resourceListManager.addToList("followedNames", "Test3", false));
|
||||
assertTrue(resourceListManager.addToList("followedNames", "Test4", false));
|
||||
assertTrue(resourceListManager.addToList("followedNames", "Test5", false));
|
||||
assertTrue(resourceListManager.addToList("followedNames", "Test6", false));
|
||||
|
||||
// Ensure the followed name count is correct
|
||||
assertEquals(4, resourceListManager.getItemCountForList("followedNames"));
|
||||
assertEquals(4, ListUtils.followedNamesCount());
|
||||
assertEquals(6, resourceListManager.getItemCountForList("followedNames"));
|
||||
assertEquals(6, ListUtils.followedNamesCount());
|
||||
|
||||
// Storage space per name should be the total storage capacity divided by the number of names
|
||||
long expectedStorageCapacityPerName = (long)(totalStorageCapacity / 4.0f);
|
||||
// then multiplied by 4, to allow for names that don't use much space
|
||||
long expectedStorageCapacityPerName = (long)(totalStorageCapacity / 6.0f) * 4L;
|
||||
assertEquals(expectedStorageCapacityPerName, storageManager.storageCapacityPerName(storageFullThreshold));
|
||||
}
|
||||
|
||||
|
180
tools/tx.pl
180
tools/tx.pl
@@ -1,16 +1,23 @@
|
||||
#!/usr/bin/env perl
|
||||
|
||||
# v4.0.2
|
||||
|
||||
use JSON;
|
||||
use warnings;
|
||||
use strict;
|
||||
|
||||
use Getopt::Std;
|
||||
use File::Basename;
|
||||
use Digest::SHA qw( sha256 sha256_hex );
|
||||
use Crypt::RIPEMD160;
|
||||
|
||||
our %opt;
|
||||
getopts('dpst', \%opt);
|
||||
|
||||
my $proc = basename($0);
|
||||
my $dirname = dirname($0);
|
||||
my $OPENSSL_SIGN = "${dirname}/openssl-sign.sh";
|
||||
my $OPENSSL_PRIV_TO_PUB = index(`$ENV{SHELL} -i -c 'openssl version;exit' 2>/dev/null`, 'OpenSSL 3.') != -1;
|
||||
|
||||
if (@ARGV < 1) {
|
||||
print STDERR "usage: $proc [-d] [-p] [-s] [-t] <tx-type> <privkey> <values> [<key-value pairs>]\n";
|
||||
@@ -24,7 +31,15 @@ if (@ARGV < 1) {
|
||||
exit 2;
|
||||
}
|
||||
|
||||
our $BASE_URL = $ENV{BASE_URL} || $opt{t} ? 'http://localhost:62391' : 'http://localhost:12391';
|
||||
our @b58 = qw{
|
||||
1 2 3 4 5 6 7 8 9
|
||||
A B C D E F G H J K L M N P Q R S T U V W X Y Z
|
||||
a b c d e f g h i j k m n o p q r s t u v w x y z
|
||||
};
|
||||
our %b58 = map { $b58[$_] => $_ } 0 .. 57;
|
||||
our %reverseb58 = reverse %b58;
|
||||
|
||||
our $BASE_URL = $ENV{BASE_URL} || ($opt{t} ? 'http://localhost:62391' : 'http://localhost:12391');
|
||||
our $DEFAULT_FEE = 0.001;
|
||||
|
||||
our %TRANSACTION_TYPES = (
|
||||
@@ -42,6 +57,7 @@ our %TRANSACTION_TYPES = (
|
||||
create_group => {
|
||||
url => 'groups/create',
|
||||
required => [qw(groupName description isOpen approvalThreshold)],
|
||||
defaults => { minimumBlockDelay => 10, maximumBlockDelay => 30 },
|
||||
key_name => 'creatorPublicKey',
|
||||
},
|
||||
update_group => {
|
||||
@@ -75,10 +91,10 @@ our %TRANSACTION_TYPES = (
|
||||
key_name => 'ownerPublicKey',
|
||||
},
|
||||
remove_group_admin => {
|
||||
url => 'groups/removeadmin',
|
||||
required => [qw(groupId txGroupId admin)],
|
||||
key_name => 'ownerPublicKey',
|
||||
},
|
||||
url => 'groups/removeadmin',
|
||||
required => [qw(groupId txGroupId member)],
|
||||
key_name => 'ownerPublicKey',
|
||||
},
|
||||
group_approval => {
|
||||
url => 'groups/approval',
|
||||
required => [qw(pendingSignature approval)],
|
||||
@@ -113,7 +129,7 @@ our %TRANSACTION_TYPES = (
|
||||
},
|
||||
update_name => {
|
||||
url => 'names/update',
|
||||
required => [qw(newName newData)],
|
||||
required => [qw(name newName newData)],
|
||||
key_name => 'ownerPublicKey',
|
||||
},
|
||||
# reward-shares
|
||||
@@ -144,13 +160,21 @@ our %TRANSACTION_TYPES = (
|
||||
key_name => 'senderPublicKey',
|
||||
pow_url => 'addresses/publicize/compute',
|
||||
},
|
||||
# Cross-chain trading
|
||||
build_trade => {
|
||||
url => 'crosschain/build',
|
||||
required => [qw(initialQortAmount finalQortAmount fundingQortAmount secretHash bitcoinAmount)],
|
||||
optional => [qw(tradeTimeout)],
|
||||
# AT
|
||||
deploy_at => {
|
||||
url => 'at',
|
||||
required => [qw(name description aTType tags creationBytes amount)],
|
||||
optional => [qw(assetId)],
|
||||
key_name => 'creatorPublicKey',
|
||||
defaults => { tradeTimeout => 10800 },
|
||||
defaults => { assetId => 0 },
|
||||
},
|
||||
# Cross-chain trading
|
||||
create_trade => {
|
||||
url => 'crosschain/tradebot/create',
|
||||
required => [qw(qortAmount fundingQortAmount foreignAmount receivingAddress)],
|
||||
optional => [qw(tradeTimeout foreignBlockchain)],
|
||||
key_name => 'creatorPublicKey',
|
||||
defaults => { tradeTimeout => 1440, foreignBlockchain => 'LITECOIN' },
|
||||
},
|
||||
trade_recipient => {
|
||||
url => 'crosschain/tradeoffer/recipient',
|
||||
@@ -196,7 +220,7 @@ if (@ARGV < @required + 1) {
|
||||
|
||||
my $priv_key = shift @ARGV;
|
||||
|
||||
my $account = account($priv_key);
|
||||
my $account;
|
||||
my $raw;
|
||||
|
||||
if ($tx_type ne 'sign') {
|
||||
@@ -215,6 +239,8 @@ if ($tx_type ne 'sign') {
|
||||
|
||||
%extras = (%extras, @ARGV);
|
||||
|
||||
$account = account($priv_key, %extras);
|
||||
|
||||
$raw = build_raw($tx_type, $account, %extras);
|
||||
printf "Raw: %s\n", $raw if $opt{d} || (!$opt{s} && !$opt{p});
|
||||
|
||||
@@ -229,7 +255,7 @@ if ($tx_type ne 'sign') {
|
||||
}
|
||||
|
||||
if ($opt{s}) {
|
||||
my $signed = sign($account->{private}, $raw);
|
||||
my $signed = sign($priv_key, $raw);
|
||||
printf "Signed: %s\n", $signed if $opt{d} || $tx_type eq 'sign';
|
||||
|
||||
if ($opt{p}) {
|
||||
@@ -246,15 +272,25 @@ if ($opt{s}) {
|
||||
}
|
||||
|
||||
sub account {
|
||||
my ($creator) = @_;
|
||||
my ($privkey, %extras) = @_;
|
||||
|
||||
my $account = { private => $creator };
|
||||
$account->{public} = api('utils/publickey', $creator);
|
||||
$account->{address} = api('addresses/convert/{publickey}', '', '{publickey}', $account->{public});
|
||||
my $account = { private => $privkey };
|
||||
$account->{public} = $extras{publickey} || priv_to_pub($privkey);
|
||||
$account->{address} = $extras{address} || pubkey_to_address($account->{public}); # api('addresses/convert/{publickey}', '', '{publickey}', $account->{public});
|
||||
|
||||
return $account;
|
||||
}
|
||||
|
||||
sub priv_to_pub {
|
||||
my ($privkey) = @_;
|
||||
|
||||
if ($OPENSSL_PRIV_TO_PUB) {
|
||||
return openssl_priv_to_pub($privkey);
|
||||
} else {
|
||||
return api('utils/publickey', $privkey);
|
||||
}
|
||||
}
|
||||
|
||||
sub build_raw {
|
||||
my ($type, $account, %extras) = @_;
|
||||
|
||||
@@ -306,6 +342,21 @@ sub build_raw {
|
||||
sub sign {
|
||||
my ($private, $raw) = @_;
|
||||
|
||||
if (-x "$OPENSSL_SIGN") {
|
||||
my $private_hex = decode_base58($private);
|
||||
chomp $private_hex;
|
||||
|
||||
my $raw_hex = decode_base58($raw);
|
||||
chomp $raw_hex;
|
||||
|
||||
my $sig = `${OPENSSL_SIGN} ${private_hex} ${raw_hex}`;
|
||||
chomp $sig;
|
||||
|
||||
my $sig58 = encode_base58(${raw_hex} . ${sig});
|
||||
chomp $sig58;
|
||||
return $sig58;
|
||||
}
|
||||
|
||||
my $json = <<" __JSON__";
|
||||
{
|
||||
"privateKey": "$private",
|
||||
@@ -344,7 +395,14 @@ sub api {
|
||||
my $curl = "curl --silent --output - --url '$BASE_URL/$url'";
|
||||
if (defined $postdata && $postdata ne '') {
|
||||
$postdata =~ tr|\n| |s;
|
||||
$curl .= " --header 'Content-Type: application/json' --data-binary '$postdata'";
|
||||
|
||||
if ($postdata =~ /^\s*\{/so) {
|
||||
$curl .= " --header 'Content-Type: application/json'";
|
||||
} else {
|
||||
$curl .= " --header 'Content-Type: text/plain'";
|
||||
}
|
||||
|
||||
$curl .= " --data-binary '$postdata'";
|
||||
$method = 'POST';
|
||||
}
|
||||
my $response = `$curl 2>/dev/null`;
|
||||
@@ -356,3 +414,87 @@ sub api {
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
sub encode_base58 {
|
||||
use integer;
|
||||
my @in = map { hex($_) } ($_[0] =~ /(..)/g);
|
||||
my $bzeros = length($1) if join('', @in) =~ /^(0*)/;
|
||||
my @out;
|
||||
my $size = 2 * scalar @in;
|
||||
for my $c (@in) {
|
||||
for (my $j = $size; $j--; ) {
|
||||
$c += 256 * ($out[$j] // 0);
|
||||
$out[$j] = $c % 58;
|
||||
$c /= 58;
|
||||
}
|
||||
}
|
||||
my $out = join('', map { $reverseb58{$_} } @out);
|
||||
return $1 if $out =~ /(1{$bzeros}[^1].*)/;
|
||||
return $1 if $out =~ /(1{$bzeros})/;
|
||||
die "Invalid base58!\n";
|
||||
}
|
||||
|
||||
|
||||
sub decode_base58 {
|
||||
use integer;
|
||||
my @out;
|
||||
my $azeros = length($1) if $_[0] =~ /^(1*)/;
|
||||
for my $c ( map { $b58{$_} } $_[0] =~ /./g ) {
|
||||
die("Invalid character!\n") unless defined $c;
|
||||
for (my $j = length($_[0]); $j--; ) {
|
||||
$c += 58 * ($out[$j] // 0);
|
||||
$out[$j] = $c % 256;
|
||||
$c /= 256;
|
||||
}
|
||||
}
|
||||
shift @out while @out && $out[0] == 0;
|
||||
unshift(@out, (0) x $azeros);
|
||||
return sprintf('%02x' x @out, @out);
|
||||
}
|
||||
|
||||
sub openssl_priv_to_pub {
|
||||
my ($privkey) = @_;
|
||||
|
||||
my $privkey_hex = decode_base58($privkey);
|
||||
|
||||
my $key_type = "04"; # hex
|
||||
my $length = "20"; # hex
|
||||
|
||||
my $asn1 = <<"__ASN1__";
|
||||
asn1=SEQUENCE:private_key
|
||||
|
||||
[private_key]
|
||||
version=INTEGER:0
|
||||
included=SEQUENCE:key_info
|
||||
raw=FORMAT:HEX,OCTETSTRING:${key_type}${length}${privkey_hex}
|
||||
|
||||
[key_info]
|
||||
type=OBJECT:ED25519
|
||||
|
||||
__ASN1__
|
||||
|
||||
my $output = `echo "${asn1}" | openssl asn1parse -i -genconf - -out - | openssl pkey -in - -inform der -noout -text_pub`;
|
||||
|
||||
# remove colons
|
||||
my $pubkey = '';
|
||||
$pubkey .= $1 while $output =~ m/([0-9a-f]{2})(?::|$)/g;
|
||||
|
||||
return encode_base58($pubkey);
|
||||
}
|
||||
|
||||
sub pubkey_to_address {
|
||||
my ($pubkey) = @_;
|
||||
|
||||
my $pubkey_hex = decode_base58($pubkey);
|
||||
my $pubkey_raw = pack('H*', $pubkey_hex);
|
||||
|
||||
my $pkh_hex = Crypt::RIPEMD160->hexhash(sha256($pubkey_raw));
|
||||
$pkh_hex =~ tr/ //ds;
|
||||
|
||||
my $version = '3a'; # hex
|
||||
|
||||
my $raw = pack('H*', $version . $pkh_hex);
|
||||
my $chksum = substr(sha256_hex(sha256($raw)), 0, 8);
|
||||
|
||||
return encode_base58($version . $pkh_hex . $chksum);
|
||||
}
|
||||
|
Reference in New Issue
Block a user