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) {
|
private PathItem getPathItemFromMethod(OpenAPI openAPI, String classPathString, Method method) {
|
||||||
Path path = method.getAnnotation(Path.class);
|
Path path = method.getAnnotation(Path.class);
|
||||||
if (path == null)
|
if (path == null)
|
||||||
throw new RuntimeException("API method has no @Path annotation?");
|
return openAPI.getPaths().get(classPathString);
|
||||||
|
|
||||||
String pathString = path.value();
|
String pathString = path.value();
|
||||||
return openAPI.getPaths().get(classPathString + pathString);
|
return openAPI.getPaths().get(classPathString + pathString);
|
||||||
|
@ -55,7 +55,6 @@ public class AssetsResource {
|
|||||||
HttpServletRequest request;
|
HttpServletRequest request;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/all")
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "List all known assets",
|
summary = "List all known assets",
|
||||||
responses = {
|
responses = {
|
||||||
|
@ -1,22 +1,33 @@
|
|||||||
package org.qora.api.resource;
|
package org.qora.api.resource;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
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.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.POST;
|
import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
import org.qora.api.ApiError;
|
import org.qora.api.ApiError;
|
||||||
import org.qora.api.ApiErrors;
|
import org.qora.api.ApiErrors;
|
||||||
import org.qora.api.ApiExceptionFactory;
|
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.data.transaction.RegisterNameTransactionData;
|
||||||
import org.qora.repository.DataException;
|
import org.qora.repository.DataException;
|
||||||
import org.qora.repository.Repository;
|
import org.qora.repository.Repository;
|
||||||
@ -28,13 +39,94 @@ import org.qora.transform.transaction.RegisterNameTransactionTransformer;
|
|||||||
import org.qora.utils.Base58;
|
import org.qora.utils.Base58;
|
||||||
|
|
||||||
@Path("/names")
|
@Path("/names")
|
||||||
@Produces({ MediaType.TEXT_PLAIN})
|
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
|
||||||
@Tag(name = "Names")
|
@Tag(name = "Names")
|
||||||
public class NamesResource {
|
public class NamesResource {
|
||||||
|
|
||||||
@Context
|
@Context
|
||||||
HttpServletRequest request;
|
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
|
@POST
|
||||||
@Path("/register")
|
@Path("/register")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -2,6 +2,11 @@ package org.qora.data.naming;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
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 {
|
public class NameData {
|
||||||
|
|
||||||
// Properties
|
// Properties
|
||||||
@ -17,6 +22,10 @@ public class NameData {
|
|||||||
|
|
||||||
// Constructors
|
// 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,
|
public NameData(byte[] registrantPublicKey, String owner, String name, String data, long registered, Long updated, byte[] reference, boolean isForSale,
|
||||||
BigDecimal salePrice) {
|
BigDecimal salePrice) {
|
||||||
this.registrantPublicKey = registrantPublicKey;
|
this.registrantPublicKey = registrantPublicKey;
|
||||||
|
@ -41,7 +41,7 @@ public abstract class TransactionData {
|
|||||||
@XmlTransient // represented in transaction-specific properties
|
@XmlTransient // represented in transaction-specific properties
|
||||||
@Schema(hidden = true)
|
@Schema(hidden = true)
|
||||||
protected byte[] creatorPublicKey;
|
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;
|
protected long timestamp;
|
||||||
@Schema(description = "sender's last transaction ID", example = "real_transaction_reference_in_base58")
|
@Schema(description = "sender's last transaction ID", example = "real_transaction_reference_in_base58")
|
||||||
protected byte[] reference;
|
protected byte[] reference;
|
||||||
@ -51,7 +51,7 @@ public abstract class TransactionData {
|
|||||||
protected byte[] signature;
|
protected byte[] signature;
|
||||||
|
|
||||||
// For JAX-RS use
|
// 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;
|
protected Integer blockHeight;
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
@ -120,6 +120,7 @@ public abstract class TransactionData {
|
|||||||
this.creatorPublicKey = creatorPublicKey;
|
this.creatorPublicKey = creatorPublicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@XmlTransient
|
||||||
public void setBlockHeight(int blockHeight) {
|
public void setBlockHeight(int blockHeight) {
|
||||||
this.blockHeight = blockHeight;
|
this.blockHeight = blockHeight;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package org.qora.repository;
|
package org.qora.repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.qora.data.naming.NameData;
|
import org.qora.data.naming.NameData;
|
||||||
|
|
||||||
public interface NameRepository {
|
public interface NameRepository {
|
||||||
@ -8,6 +10,10 @@ public interface NameRepository {
|
|||||||
|
|
||||||
public boolean nameExists(String name) throws DataException;
|
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 save(NameData nameData) throws DataException;
|
||||||
|
|
||||||
public void delete(String name) throws DataException;
|
public void delete(String name) throws DataException;
|
||||||
|
@ -4,7 +4,9 @@ import java.math.BigDecimal;
|
|||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.qora.data.naming.NameData;
|
import org.qora.data.naming.NameData;
|
||||||
import org.qora.repository.DataException;
|
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
|
@Override
|
||||||
public void save(NameData nameData) throws DataException {
|
public void save(NameData nameData) throws DataException {
|
||||||
HSQLDBSaver saveHelper = new HSQLDBSaver("Names");
|
HSQLDBSaver saveHelper = new HSQLDBSaver("Names");
|
||||||
|
@ -8,6 +8,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.List;
|
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.PaymentData;
|
||||||
import org.qora.data.transaction.TransactionData;
|
import org.qora.data.transaction.TransactionData;
|
||||||
import org.qora.repository.DataException;
|
import org.qora.repository.DataException;
|
||||||
@ -18,6 +20,8 @@ import org.qora.transaction.Transaction.TransactionType;
|
|||||||
|
|
||||||
public class HSQLDBTransactionRepository implements TransactionRepository {
|
public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(HSQLDBTransactionRepository.class);
|
||||||
|
|
||||||
protected HSQLDBRepository repository;
|
protected HSQLDBRepository repository;
|
||||||
private HSQLDBGenesisTransactionRepository genesisTransactionRepository;
|
private HSQLDBGenesisTransactionRepository genesisTransactionRepository;
|
||||||
private HSQLDBPaymentTransactionRepository paymentTransactionRepository;
|
private HSQLDBPaymentTransactionRepository paymentTransactionRepository;
|
||||||
@ -314,34 +318,24 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
|||||||
|
|
||||||
String signatureColumn = "NULL";
|
String signatureColumn = "NULL";
|
||||||
List<Object> bindParams = new ArrayList<Object>();
|
List<Object> bindParams = new ArrayList<Object>();
|
||||||
|
String groupBy = "";
|
||||||
|
|
||||||
// Table JOINs first
|
// Table JOINs first
|
||||||
List<String> tableJoins = new ArrayList<String>();
|
List<String> tableJoins = new ArrayList<String>();
|
||||||
|
|
||||||
if (hasHeightRange) {
|
// Always JOIN BlockTransactions as we only ever want confirmed transactions
|
||||||
tableJoins.add("Blocks");
|
tableJoins.add("Blocks");
|
||||||
tableJoins.add("BlockTransactions ON BlockTransactions.block_signature = Blocks.signature");
|
tableJoins.add("BlockTransactions ON BlockTransactions.block_signature = Blocks.signature");
|
||||||
signatureColumn = "BlockTransactions.transaction_signature";
|
signatureColumn = "BlockTransactions.transaction_signature";
|
||||||
}
|
|
||||||
|
|
||||||
if (hasTxType) {
|
// Always JOIN Transactions as we want to order by timestamp
|
||||||
if (hasHeightRange)
|
tableJoins.add("Transactions ON Transactions.signature = BlockTransactions.transaction_signature");
|
||||||
tableJoins.add("Transactions ON Transactions.signature = BlockTransactions.transaction_signature");
|
signatureColumn = "Transactions.signature";
|
||||||
else
|
|
||||||
tableJoins.add("Transactions");
|
|
||||||
|
|
||||||
signatureColumn = "Transactions.signature";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasAddress) {
|
if (hasAddress) {
|
||||||
if (hasTxType)
|
tableJoins.add("TransactionParticipants ON TransactionParticipants.signature = Transactions.signature");
|
||||||
tableJoins.add("TransactionParticipants ON TransactionParticipants.signature = Transactions.signature");
|
|
||||||
else if (hasHeightRange)
|
|
||||||
tableJoins.add("TransactionParticipants ON TransactionParticipants.signature = BlockTransactions.transaction_signature");
|
|
||||||
else
|
|
||||||
tableJoins.add("TransactionParticipants");
|
|
||||||
|
|
||||||
signatureColumn = "TransactionParticipants.signature";
|
signatureColumn = "TransactionParticipants.signature";
|
||||||
|
groupBy = " GROUP BY TransactionParticipants.signature, Transactions.creation";
|
||||||
}
|
}
|
||||||
|
|
||||||
// WHERE clauses next
|
// WHERE clauses next
|
||||||
@ -362,7 +356,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
|||||||
bindParams.add(address);
|
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())) {
|
try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams.toArray())) {
|
||||||
if (resultSet == null)
|
if (resultSet == null)
|
||||||
|
@ -28,6 +28,10 @@ public class Settings {
|
|||||||
private boolean useBitcoinTestNet = false;
|
private boolean useBitcoinTestNet = false;
|
||||||
private boolean wipeUnconfirmedOnStart = true;
|
private boolean wipeUnconfirmedOnStart = true;
|
||||||
private String blockchainConfigPath = "blockchain.json";
|
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
|
// RPC
|
||||||
private int rpcPort = 9085;
|
private int rpcPort = 9085;
|
||||||
@ -131,11 +135,19 @@ public class Settings {
|
|||||||
if (json.containsKey("rpcenabled"))
|
if (json.containsKey("rpcenabled"))
|
||||||
this.rpcEnabled = ((Boolean) json.get("rpcenabled")).booleanValue();
|
this.rpcEnabled = ((Boolean) json.get("rpcenabled")).booleanValue();
|
||||||
|
|
||||||
// Blockchain config
|
// Node-specific behaviour
|
||||||
|
|
||||||
if (json.containsKey("wipeUnconfirmedOnStart"))
|
if (json.containsKey("wipeUnconfirmedOnStart"))
|
||||||
this.wipeUnconfirmedOnStart = (Boolean) getTypedJson(json, "wipeUnconfirmedOnStart", Boolean.class);
|
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"))
|
if (json.containsKey("blockchainConfig"))
|
||||||
blockchainConfigPath = (String) getTypedJson(json, "blockchainConfig", String.class);
|
blockchainConfigPath = (String) getTypedJson(json, "blockchainConfig", String.class);
|
||||||
|
|
||||||
@ -182,6 +194,14 @@ public class Settings {
|
|||||||
return this.wipeUnconfirmedOnStart;
|
return this.wipeUnconfirmedOnStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getMaxUnconfirmedPerAccount() {
|
||||||
|
return this.maxUnconfirmedPerAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMaxTransactionTimestampFuture() {
|
||||||
|
return this.maxTransactionTimestampFuture;
|
||||||
|
}
|
||||||
|
|
||||||
// Config parsing
|
// Config parsing
|
||||||
|
|
||||||
public static Object getTypedJson(JSONObject json, String key, Class<?> clazz) {
|
public static Object getTypedJson(JSONObject json, String key, Class<?> clazz) {
|
||||||
|
@ -55,7 +55,7 @@ public class CancelOrderTransaction extends Transaction {
|
|||||||
// Navigation
|
// Navigation
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Account getCreator() throws DataException {
|
public PublicKeyAccount getCreator() throws DataException {
|
||||||
return new PublicKeyAccount(this.repository, cancelOrderTransactionData.getCreatorPublicKey());
|
return new PublicKeyAccount(this.repository, cancelOrderTransactionData.getCreatorPublicKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ public class CreateOrderTransaction extends Transaction {
|
|||||||
// Navigation
|
// Navigation
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Account getCreator() throws DataException {
|
public PublicKeyAccount getCreator() throws DataException {
|
||||||
return new PublicKeyAccount(this.repository, createOrderTransactionData.getCreatorPublicKey());
|
return new PublicKeyAccount(this.repository, createOrderTransactionData.getCreatorPublicKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ public class CreatePollTransaction extends Transaction {
|
|||||||
// Navigation
|
// Navigation
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Account getCreator() throws DataException {
|
public PublicKeyAccount getCreator() throws DataException {
|
||||||
return new PublicKeyAccount(this.repository, this.createPollTransactionData.getCreatorPublicKey());
|
return new PublicKeyAccount(this.repository, this.createPollTransactionData.getCreatorPublicKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import java.math.BigDecimal;
|
|||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.math.MathContext;
|
import java.math.MathContext;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -18,6 +19,7 @@ import org.qora.data.block.BlockData;
|
|||||||
import org.qora.data.transaction.TransactionData;
|
import org.qora.data.transaction.TransactionData;
|
||||||
import org.qora.repository.DataException;
|
import org.qora.repository.DataException;
|
||||||
import org.qora.repository.Repository;
|
import org.qora.repository.Repository;
|
||||||
|
import org.qora.settings.Settings;
|
||||||
import org.qora.transform.TransformationException;
|
import org.qora.transform.TransformationException;
|
||||||
import org.qora.transform.transaction.TransactionTransformer;
|
import org.qora.transform.transaction.TransactionTransformer;
|
||||||
import org.qora.utils.Base58;
|
import org.qora.utils.Base58;
|
||||||
@ -109,6 +111,9 @@ public abstract class Transaction {
|
|||||||
ASSET_DOES_NOT_MATCH_AT(41),
|
ASSET_DOES_NOT_MATCH_AT(41),
|
||||||
ASSET_ALREADY_EXISTS(43),
|
ASSET_ALREADY_EXISTS(43),
|
||||||
MISSING_CREATOR(44),
|
MISSING_CREATOR(44),
|
||||||
|
TIMESTAMP_TOO_OLD(45),
|
||||||
|
TIMESTAMP_TOO_NEW(46),
|
||||||
|
TOO_MANY_UNCONFIRMED(47),
|
||||||
NOT_YET_RELEASED(1000);
|
NOT_YET_RELEASED(1000);
|
||||||
|
|
||||||
public final int value;
|
public final int value;
|
||||||
@ -364,7 +369,7 @@ public abstract class Transaction {
|
|||||||
* @return creator
|
* @return creator
|
||||||
* @throws DataException
|
* @throws DataException
|
||||||
*/
|
*/
|
||||||
protected Account getCreator() throws DataException {
|
protected PublicKeyAccount getCreator() throws DataException {
|
||||||
if (this.transactionData.getCreatorPublicKey() == null)
|
if (this.transactionData.getCreatorPublicKey() == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@ -434,18 +439,49 @@ public abstract class Transaction {
|
|||||||
* @throws DataException
|
* @throws DataException
|
||||||
*/
|
*/
|
||||||
public ValidationResult isValidUnconfirmed() 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 {
|
try {
|
||||||
Account creator = this.getCreator();
|
PublicKeyAccount creator = this.getCreator();
|
||||||
if (creator == null)
|
if (creator == null)
|
||||||
return ValidationResult.MISSING_CREATOR;
|
return ValidationResult.MISSING_CREATOR;
|
||||||
|
|
||||||
creator.setLastReference(creator.getUnconfirmedLastReference());
|
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 {
|
} finally {
|
||||||
repository.discardChanges();
|
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.
|
* Returns sorted, unconfirmed transactions, deleting invalid.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -16,6 +16,7 @@ public final class NTP {
|
|||||||
private static long lastUpdate = 0;
|
private static long lastUpdate = 0;
|
||||||
private static long offset = 0;
|
private static long offset = 0;
|
||||||
|
|
||||||
|
/** Returns NTP-synced current time from unix epoch, in milliseconds. */
|
||||||
public static long getTime() {
|
public static long getTime() {
|
||||||
// Every so often use NTP to find out offset between this system's time and internet time
|
// Every so often use NTP to find out offset between this system's time and internet time
|
||||||
if (System.currentTimeMillis() > lastUpdate + TIME_TILL_UPDATE) {
|
if (System.currentTimeMillis() > lastUpdate + TIME_TILL_UPDATE) {
|
||||||
|
Loading…
Reference in New Issue
Block a user