forked from Qortal/qortal
API + new tx restrictions
Added GET /names to list all registered name. Added GET /names/{name} for more info on a specific name. Added GET /names/address/{address} for names owned by address. Renamed GET /assets/all to GET /assets in line with above. Fixed edge cases with AnnotationPostProcessor. Fixed incorrectly exposed "blockHeight" in API UI examples/values. Changed example transaction timestamp. Added checks on building/signing/processing new transactions via API so that they are not too old (older than latest block's timestamp), too new (more than 24 hours in the future) or the tx creator doesn't already have a lot of existing unconfirmed transactions (default 100). Configurable via settings.json properties maxUnconfirmedPerAccount and maxTransactionTimestampFuture. Improved /transactions/search to not return unconfirmed transactions and to order by timestamp. Transaction.getCreator() now returns PublicKeyAccount, not Account.
This commit is contained in:
parent
7998166c0a
commit
95d640cc8c
31
src/main/java/org/qora/api/model/NameSummary.java
Normal file
31
src/main/java/org/qora/api/model/NameSummary.java
Normal file
@ -0,0 +1,31 @@
|
||||
package org.qora.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
|
||||
import org.qora.data.naming.NameData;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.NONE)
|
||||
public class NameSummary {
|
||||
|
||||
private NameData nameData;
|
||||
|
||||
protected NameSummary() {
|
||||
}
|
||||
|
||||
public NameSummary(NameData nameData) {
|
||||
this.nameData = nameData;
|
||||
}
|
||||
|
||||
@XmlElement(name = "name")
|
||||
public String getName() {
|
||||
return this.nameData.getName();
|
||||
}
|
||||
|
||||
@XmlElement(name = "owner")
|
||||
public String getOwner() {
|
||||
return this.nameData.getOwner();
|
||||
}
|
||||
|
||||
}
|
@ -79,7 +79,7 @@ public class AnnotationPostProcessor implements ReaderListener {
|
||||
private PathItem getPathItemFromMethod(OpenAPI openAPI, String classPathString, Method method) {
|
||||
Path path = method.getAnnotation(Path.class);
|
||||
if (path == null)
|
||||
throw new RuntimeException("API method has no @Path annotation?");
|
||||
return openAPI.getPaths().get(classPathString);
|
||||
|
||||
String pathString = path.value();
|
||||
return openAPI.getPaths().get(classPathString + pathString);
|
||||
|
@ -55,7 +55,6 @@ public class AssetsResource {
|
||||
HttpServletRequest request;
|
||||
|
||||
@GET
|
||||
@Path("/all")
|
||||
@Operation(
|
||||
summary = "List all known assets",
|
||||
responses = {
|
||||
|
@ -1,22 +1,33 @@
|
||||
package org.qora.api.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.qora.api.ApiError;
|
||||
import org.qora.api.ApiErrors;
|
||||
import org.qora.api.ApiExceptionFactory;
|
||||
import org.qora.api.model.NameSummary;
|
||||
import org.qora.crypto.Crypto;
|
||||
import org.qora.data.naming.NameData;
|
||||
import org.qora.data.transaction.RegisterNameTransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
@ -28,13 +39,94 @@ import org.qora.transform.transaction.RegisterNameTransactionTransformer;
|
||||
import org.qora.utils.Base58;
|
||||
|
||||
@Path("/names")
|
||||
@Produces({ MediaType.TEXT_PLAIN})
|
||||
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
|
||||
@Tag(name = "Names")
|
||||
public class NamesResource {
|
||||
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
|
||||
@GET
|
||||
@Operation(
|
||||
summary = "List all registered names",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "registered name info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
array = @ArraySchema(schema = @Schema(implementation = NameSummary.class))
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public List<NameSummary> getAllNames(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<NameData> names = repository.getNameRepository().getAllNames();
|
||||
|
||||
// Pagination would take effect here (or as part of the repository access)
|
||||
int fromIndex = Integer.min(offset, names.size());
|
||||
int toIndex = limit == 0 ? names.size() : Integer.min(fromIndex + limit, names.size());
|
||||
names = names.subList(fromIndex, toIndex);
|
||||
|
||||
return names.stream().map(nameData -> new NameSummary(nameData)).collect(Collectors.toList());
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/address/{address}")
|
||||
@Operation(
|
||||
summary = "List all names owned by address",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "registered name info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
array = @ArraySchema(schema = @Schema(implementation = NameSummary.class))
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public List<NameSummary> getNamesByAddress(@PathParam("address") String address) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<NameData> names = repository.getNameRepository().getNamesByOwner(address);
|
||||
|
||||
return names.stream().map(nameData -> new NameSummary(nameData)).collect(Collectors.toList());
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{name}")
|
||||
@Operation(
|
||||
summary = "Info on registered name",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "registered name info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(implementation = NameData.class)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public NameData getName(@PathParam("name") String name) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getNameRepository().fromName(name);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@POST
|
||||
@Path("/register")
|
||||
@Operation(
|
||||
|
@ -2,6 +2,11 @@ package org.qora.data.naming;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
// All properties to be converted to JSON via JAX-RS
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class NameData {
|
||||
|
||||
// Properties
|
||||
@ -17,6 +22,10 @@ public class NameData {
|
||||
|
||||
// Constructors
|
||||
|
||||
// necessary for JAX-RS serialization
|
||||
protected NameData() {
|
||||
}
|
||||
|
||||
public NameData(byte[] registrantPublicKey, String owner, String name, String data, long registered, Long updated, byte[] reference, boolean isForSale,
|
||||
BigDecimal salePrice) {
|
||||
this.registrantPublicKey = registrantPublicKey;
|
||||
|
@ -41,7 +41,7 @@ public abstract class TransactionData {
|
||||
@XmlTransient // represented in transaction-specific properties
|
||||
@Schema(hidden = true)
|
||||
protected byte[] creatorPublicKey;
|
||||
@Schema(description = "timestamp when transaction created, in milliseconds since unix epoch", example = "1545062012000")
|
||||
@Schema(description = "timestamp when transaction created, in milliseconds since unix epoch", example = "__unix_epoch_time_milliseconds__")
|
||||
protected long timestamp;
|
||||
@Schema(description = "sender's last transaction ID", example = "real_transaction_reference_in_base58")
|
||||
protected byte[] reference;
|
||||
@ -51,7 +51,7 @@ public abstract class TransactionData {
|
||||
protected byte[] signature;
|
||||
|
||||
// For JAX-RS use
|
||||
@Schema(accessMode = AccessMode.READ_ONLY, description = "height of block containing transaction")
|
||||
@Schema(accessMode = AccessMode.READ_ONLY, hidden = true, description = "height of block containing transaction")
|
||||
protected Integer blockHeight;
|
||||
|
||||
// Constructors
|
||||
@ -120,6 +120,7 @@ public abstract class TransactionData {
|
||||
this.creatorPublicKey = creatorPublicKey;
|
||||
}
|
||||
|
||||
@XmlTransient
|
||||
public void setBlockHeight(int blockHeight) {
|
||||
this.blockHeight = blockHeight;
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.qora.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.qora.data.naming.NameData;
|
||||
|
||||
public interface NameRepository {
|
||||
@ -8,6 +10,10 @@ public interface NameRepository {
|
||||
|
||||
public boolean nameExists(String name) throws DataException;
|
||||
|
||||
public List<NameData> getAllNames() throws DataException;
|
||||
|
||||
public List<NameData> getNamesByOwner(String address) throws DataException;
|
||||
|
||||
public void save(NameData nameData) throws DataException;
|
||||
|
||||
public void delete(String name) throws DataException;
|
||||
|
@ -4,7 +4,9 @@ import java.math.BigDecimal;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import org.qora.data.naming.NameData;
|
||||
import org.qora.repository.DataException;
|
||||
@ -53,6 +55,71 @@ public class HSQLDBNameRepository implements NameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NameData> getAllNames() throws DataException {
|
||||
List<NameData> names = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository
|
||||
.checkedExecute("SELECT name, data, registrant, owner, registered, updated, reference, is_for_sale, sale_price FROM Names")) {
|
||||
if (resultSet == null)
|
||||
return names;
|
||||
|
||||
do {
|
||||
String name = resultSet.getString(1);
|
||||
String data = resultSet.getString(2);
|
||||
byte[] registrantPublicKey = resultSet.getBytes(3);
|
||||
String owner = resultSet.getString(4);
|
||||
long registered = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
|
||||
|
||||
// Special handling for possibly-NULL "updated" column
|
||||
Timestamp updatedTimestamp = resultSet.getTimestamp(6, Calendar.getInstance(HSQLDBRepository.UTC));
|
||||
Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime();
|
||||
|
||||
byte[] reference = resultSet.getBytes(7);
|
||||
boolean isForSale = resultSet.getBoolean(8);
|
||||
BigDecimal salePrice = resultSet.getBigDecimal(9);
|
||||
|
||||
names.add(new NameData(registrantPublicKey, owner, name, data, registered, updated, reference, isForSale, salePrice));
|
||||
} while (resultSet.next());
|
||||
|
||||
return names;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch names from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NameData> getNamesByOwner(String owner) throws DataException {
|
||||
List<NameData> names = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository
|
||||
.checkedExecute("SELECT name, data, registrant, registered, updated, reference, is_for_sale, sale_price FROM Names WHERE owner = ?", owner)) {
|
||||
if (resultSet == null)
|
||||
return names;
|
||||
|
||||
do {
|
||||
String name = resultSet.getString(1);
|
||||
String data = resultSet.getString(2);
|
||||
byte[] registrantPublicKey = resultSet.getBytes(3);
|
||||
long registered = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
|
||||
|
||||
// Special handling for possibly-NULL "updated" column
|
||||
Timestamp updatedTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC));
|
||||
Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime();
|
||||
|
||||
byte[] reference = resultSet.getBytes(6);
|
||||
boolean isForSale = resultSet.getBoolean(7);
|
||||
BigDecimal salePrice = resultSet.getBigDecimal(8);
|
||||
|
||||
names.add(new NameData(registrantPublicKey, owner, name, data, registered, updated, reference, isForSale, salePrice));
|
||||
} while (resultSet.next());
|
||||
|
||||
return names;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch account's names from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(NameData nameData) throws DataException {
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("Names");
|
||||
|
@ -8,6 +8,8 @@ import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qora.data.PaymentData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
@ -18,6 +20,8 @@ import org.qora.transaction.Transaction.TransactionType;
|
||||
|
||||
public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HSQLDBTransactionRepository.class);
|
||||
|
||||
protected HSQLDBRepository repository;
|
||||
private HSQLDBGenesisTransactionRepository genesisTransactionRepository;
|
||||
private HSQLDBPaymentTransactionRepository paymentTransactionRepository;
|
||||
@ -314,34 +318,24 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
|
||||
String signatureColumn = "NULL";
|
||||
List<Object> bindParams = new ArrayList<Object>();
|
||||
String groupBy = "";
|
||||
|
||||
// Table JOINs first
|
||||
List<String> tableJoins = new ArrayList<String>();
|
||||
|
||||
if (hasHeightRange) {
|
||||
tableJoins.add("Blocks");
|
||||
tableJoins.add("BlockTransactions ON BlockTransactions.block_signature = Blocks.signature");
|
||||
signatureColumn = "BlockTransactions.transaction_signature";
|
||||
}
|
||||
// Always JOIN BlockTransactions as we only ever want confirmed transactions
|
||||
tableJoins.add("Blocks");
|
||||
tableJoins.add("BlockTransactions ON BlockTransactions.block_signature = Blocks.signature");
|
||||
signatureColumn = "BlockTransactions.transaction_signature";
|
||||
|
||||
if (hasTxType) {
|
||||
if (hasHeightRange)
|
||||
tableJoins.add("Transactions ON Transactions.signature = BlockTransactions.transaction_signature");
|
||||
else
|
||||
tableJoins.add("Transactions");
|
||||
|
||||
signatureColumn = "Transactions.signature";
|
||||
}
|
||||
// Always JOIN Transactions as we want to order by timestamp
|
||||
tableJoins.add("Transactions ON Transactions.signature = BlockTransactions.transaction_signature");
|
||||
signatureColumn = "Transactions.signature";
|
||||
|
||||
if (hasAddress) {
|
||||
if (hasTxType)
|
||||
tableJoins.add("TransactionParticipants ON TransactionParticipants.signature = Transactions.signature");
|
||||
else if (hasHeightRange)
|
||||
tableJoins.add("TransactionParticipants ON TransactionParticipants.signature = BlockTransactions.transaction_signature");
|
||||
else
|
||||
tableJoins.add("TransactionParticipants");
|
||||
|
||||
tableJoins.add("TransactionParticipants ON TransactionParticipants.signature = Transactions.signature");
|
||||
signatureColumn = "TransactionParticipants.signature";
|
||||
groupBy = " GROUP BY TransactionParticipants.signature, Transactions.creation";
|
||||
}
|
||||
|
||||
// WHERE clauses next
|
||||
@ -362,7 +356,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
bindParams.add(address);
|
||||
}
|
||||
|
||||
String sql = "SELECT " + signatureColumn + " FROM " + String.join(" JOIN ", tableJoins) + " WHERE " + String.join(" AND ", whereClauses);
|
||||
String sql = "SELECT " + signatureColumn + " FROM " + String.join(" JOIN ", tableJoins) + " WHERE " + String.join(" AND ", whereClauses) + groupBy + " ORDER BY Transactions.creation ASC";
|
||||
LOGGER.trace(sql);
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams.toArray())) {
|
||||
if (resultSet == null)
|
||||
|
@ -28,6 +28,10 @@ public class Settings {
|
||||
private boolean useBitcoinTestNet = false;
|
||||
private boolean wipeUnconfirmedOnStart = true;
|
||||
private String blockchainConfigPath = "blockchain.json";
|
||||
/** Maximum number of unconfirmed transactions allowed per account */
|
||||
private int maxUnconfirmedPerAccount = 100;
|
||||
/** Max milliseconds into future for accepting new, unconfirmed transactions */
|
||||
private long maxTransactionTimestampFuture = 24 * 60 * 60 * 1000; // milliseconds
|
||||
|
||||
// RPC
|
||||
private int rpcPort = 9085;
|
||||
@ -131,11 +135,19 @@ public class Settings {
|
||||
if (json.containsKey("rpcenabled"))
|
||||
this.rpcEnabled = ((Boolean) json.get("rpcenabled")).booleanValue();
|
||||
|
||||
// Blockchain config
|
||||
// Node-specific behaviour
|
||||
|
||||
if (json.containsKey("wipeUnconfirmedOnStart"))
|
||||
this.wipeUnconfirmedOnStart = (Boolean) getTypedJson(json, "wipeUnconfirmedOnStart", Boolean.class);
|
||||
|
||||
if (json.containsKey("maxUnconfirmedPerAccount"))
|
||||
this.maxUnconfirmedPerAccount = ((Long) getTypedJson(json, "maxUnconfirmedPerAccount", Long.class)).intValue();
|
||||
|
||||
if (json.containsKey("maxTransactionTimestampFuture"))
|
||||
this.maxTransactionTimestampFuture = (Long) getTypedJson(json, "maxTransactionTimestampFuture", Long.class);
|
||||
|
||||
// Blockchain config
|
||||
|
||||
if (json.containsKey("blockchainConfig"))
|
||||
blockchainConfigPath = (String) getTypedJson(json, "blockchainConfig", String.class);
|
||||
|
||||
@ -182,6 +194,14 @@ public class Settings {
|
||||
return this.wipeUnconfirmedOnStart;
|
||||
}
|
||||
|
||||
public int getMaxUnconfirmedPerAccount() {
|
||||
return this.maxUnconfirmedPerAccount;
|
||||
}
|
||||
|
||||
public long getMaxTransactionTimestampFuture() {
|
||||
return this.maxTransactionTimestampFuture;
|
||||
}
|
||||
|
||||
// Config parsing
|
||||
|
||||
public static Object getTypedJson(JSONObject json, String key, Class<?> clazz) {
|
||||
|
@ -55,7 +55,7 @@ public class CancelOrderTransaction extends Transaction {
|
||||
// Navigation
|
||||
|
||||
@Override
|
||||
public Account getCreator() throws DataException {
|
||||
public PublicKeyAccount getCreator() throws DataException {
|
||||
return new PublicKeyAccount(this.repository, cancelOrderTransactionData.getCreatorPublicKey());
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@ public class CreateOrderTransaction extends Transaction {
|
||||
// Navigation
|
||||
|
||||
@Override
|
||||
public Account getCreator() throws DataException {
|
||||
public PublicKeyAccount getCreator() throws DataException {
|
||||
return new PublicKeyAccount(this.repository, createOrderTransactionData.getCreatorPublicKey());
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ public class CreatePollTransaction extends Transaction {
|
||||
// Navigation
|
||||
|
||||
@Override
|
||||
public Account getCreator() throws DataException {
|
||||
public PublicKeyAccount getCreator() throws DataException {
|
||||
return new PublicKeyAccount(this.repository, this.createPollTransactionData.getCreatorPublicKey());
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.MathContext;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -18,6 +19,7 @@ import org.qora.data.block.BlockData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.settings.Settings;
|
||||
import org.qora.transform.TransformationException;
|
||||
import org.qora.transform.transaction.TransactionTransformer;
|
||||
import org.qora.utils.Base58;
|
||||
@ -109,6 +111,9 @@ public abstract class Transaction {
|
||||
ASSET_DOES_NOT_MATCH_AT(41),
|
||||
ASSET_ALREADY_EXISTS(43),
|
||||
MISSING_CREATOR(44),
|
||||
TIMESTAMP_TOO_OLD(45),
|
||||
TIMESTAMP_TOO_NEW(46),
|
||||
TOO_MANY_UNCONFIRMED(47),
|
||||
NOT_YET_RELEASED(1000);
|
||||
|
||||
public final int value;
|
||||
@ -364,7 +369,7 @@ public abstract class Transaction {
|
||||
* @return creator
|
||||
* @throws DataException
|
||||
*/
|
||||
protected Account getCreator() throws DataException {
|
||||
protected PublicKeyAccount getCreator() throws DataException {
|
||||
if (this.transactionData.getCreatorPublicKey() == null)
|
||||
return null;
|
||||
|
||||
@ -434,18 +439,49 @@ public abstract class Transaction {
|
||||
* @throws DataException
|
||||
*/
|
||||
public ValidationResult isValidUnconfirmed() throws DataException {
|
||||
// Transactions with a timestamp prior to latest block's timestamp are too old
|
||||
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||
if (this.transactionData.getTimestamp() <= latestBlock.getTimestamp())
|
||||
return ValidationResult.TIMESTAMP_TOO_OLD;
|
||||
|
||||
// Transactions with a timestamp too far into future are too new
|
||||
long maxTimestamp = NTP.getTime() + Settings.getInstance().getMaxTransactionTimestampFuture();
|
||||
if (this.transactionData.getTimestamp() > maxTimestamp)
|
||||
return ValidationResult.TIMESTAMP_TOO_NEW;
|
||||
|
||||
try {
|
||||
Account creator = this.getCreator();
|
||||
PublicKeyAccount creator = this.getCreator();
|
||||
if (creator == null)
|
||||
return ValidationResult.MISSING_CREATOR;
|
||||
|
||||
creator.setLastReference(creator.getUnconfirmedLastReference());
|
||||
return this.isValid();
|
||||
ValidationResult result = this.isValid();
|
||||
|
||||
// Reject if unconfirmed pile already has X transactions from same creator
|
||||
if (result == ValidationResult.OK && countUnconfirmedByCreator(creator) >= Settings.getInstance().getMaxUnconfirmedPerAccount())
|
||||
return ValidationResult.TOO_MANY_UNCONFIRMED;
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
repository.discardChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private int countUnconfirmedByCreator(PublicKeyAccount creator) throws DataException {
|
||||
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions();
|
||||
|
||||
int count = 0;
|
||||
for (TransactionData transactionData : unconfirmedTransactions) {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
PublicKeyAccount otherCreator = transaction.getCreator();
|
||||
|
||||
if (Arrays.equals(creator.getPublicKey(), otherCreator.getPublicKey()))
|
||||
++count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sorted, unconfirmed transactions, deleting invalid.
|
||||
* <p>
|
||||
|
@ -16,6 +16,7 @@ public final class NTP {
|
||||
private static long lastUpdate = 0;
|
||||
private static long offset = 0;
|
||||
|
||||
/** Returns NTP-synced current time from unix epoch, in milliseconds. */
|
||||
public static long getTime() {
|
||||
// Every so often use NTP to find out offset between this system's time and internet time
|
||||
if (System.currentTimeMillis() > lastUpdate + TIME_TILL_UPDATE) {
|
||||
|
Loading…
Reference in New Issue
Block a user