diff --git a/src/main/java/org/qortal/api/ApiError.java b/src/main/java/org/qortal/api/ApiError.java index 659104e7..b52332b1 100644 --- a/src/main/java/org/qortal/api/ApiError.java +++ b/src/main/java/org/qortal/api/ApiError.java @@ -79,7 +79,7 @@ public enum ApiError { // BUYER_ALREADY_OWNER(411, 422), // POLLS - // POLL_NO_EXISTS(501, 404), + POLL_NO_EXISTS(501, 404), // POLL_ALREADY_EXISTS(502, 422), // DUPLICATE_OPTION(503, 422), // POLL_OPTION_NO_EXISTS(504, 404), diff --git a/src/main/java/org/qortal/api/resource/AdminResource.java b/src/main/java/org/qortal/api/resource/AdminResource.java index ef2a3f95..154f9159 100644 --- a/src/main/java/org/qortal/api/resource/AdminResource.java +++ b/src/main/java/org/qortal/api/resource/AdminResource.java @@ -153,6 +153,22 @@ public class AdminResource { return nodeStatus; } + @GET + @Path("/settings") + @Operation( + summary = "Fetch node settings", + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Settings.class)) + ) + } + ) + public Settings settings() { + Settings nodeSettings = Settings.getInstance(); + + return nodeSettings; + } + @GET @Path("/stop") @Operation( diff --git a/src/main/java/org/qortal/api/resource/PollsResource.java b/src/main/java/org/qortal/api/resource/PollsResource.java new file mode 100644 index 00000000..952cbdc5 --- /dev/null +++ b/src/main/java/org/qortal/api/resource/PollsResource.java @@ -0,0 +1,197 @@ +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.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.qortal.api.ApiError; +import org.qortal.api.ApiErrors; +import org.qortal.api.ApiExceptionFactory; +import org.qortal.data.transaction.CreatePollTransactionData; +import org.qortal.data.transaction.PaymentTransactionData; +import org.qortal.data.transaction.VoteOnPollTransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; +import org.qortal.transaction.Transaction; +import org.qortal.transform.TransformationException; +import org.qortal.transform.transaction.CreatePollTransactionTransformer; +import org.qortal.transform.transaction.PaymentTransactionTransformer; +import org.qortal.transform.transaction.VoteOnPollTransactionTransformer; +import org.qortal.utils.Base58; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import java.util.List; +import javax.ws.rs.GET; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; +import org.qortal.api.ApiException; +import org.qortal.data.voting.PollData; + +@Path("/polls") +@Tag(name = "Polls") +public class PollsResource { + @Context + HttpServletRequest request; + + @GET + @Operation( + summary = "List all polls", + responses = { + @ApiResponse( + description = "poll info", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + array = @ArraySchema(schema = @Schema(implementation = PollData.class)) + ) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public List getAllPolls(@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 allPollData = repository.getVotingRepository().getAllPolls(limit, offset, reverse); + return allPollData; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @GET + @Path("/{pollName}") + @Operation( + summary = "Info on poll", + responses = { + @ApiResponse( + description = "poll info", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = PollData.class) + ) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public PollData getPollData(@PathParam("pollName") String pollName) { + try (final Repository repository = RepositoryManager.getRepository()) { + PollData pollData = repository.getVotingRepository().fromPollName(pollName); + if (pollData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.POLL_NO_EXISTS); + + return pollData; + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST + @Path("/create") + @Operation( + summary = "Build raw, unsigned, CREATE_POLL transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = CreatePollTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, CREATE_POLL transaction encoded in Base58", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + public String CreatePoll(CreatePollTransactionData transactionData) { + if (Settings.getInstance().isApiRestricted()) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); + + try (final Repository repository = RepositoryManager.getRepository()) { + Transaction transaction = Transaction.fromData(repository, transactionData); + + Transaction.ValidationResult result = transaction.isValidUnconfirmed(); + if (result != Transaction.ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + byte[] bytes = CreatePollTransactionTransformer.toBytes(transactionData); + return Base58.encode(bytes); + } catch (TransformationException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST + @Path("/vote") + @Operation( + summary = "Build raw, unsigned, VOTE_ON_POLL transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = VoteOnPollTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, VOTE_ON_POLL transaction encoded in Base58", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + public String VoteOnPoll(VoteOnPollTransactionData transactionData) { + if (Settings.getInstance().isApiRestricted()) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); + + try (final Repository repository = RepositoryManager.getRepository()) { + Transaction transaction = Transaction.fromData(repository, transactionData); + + Transaction.ValidationResult result = transaction.isValidUnconfirmed(); + if (result != Transaction.ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + byte[] bytes = VoteOnPollTransactionTransformer.toBytes(transactionData); + return Base58.encode(bytes); + } catch (TransformationException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + +} diff --git a/src/main/java/org/qortal/data/transaction/CreatePollTransactionData.java b/src/main/java/org/qortal/data/transaction/CreatePollTransactionData.java index 4df7d79d..8b904aa0 100644 --- a/src/main/java/org/qortal/data/transaction/CreatePollTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/CreatePollTransactionData.java @@ -2,9 +2,11 @@ package org.qortal.data.transaction; import java.util.List; +import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue; import org.qortal.data.voting.PollOptionData; import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction.TransactionType; @@ -14,8 +16,13 @@ import io.swagger.v3.oas.annotations.media.Schema; // All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) @Schema(allOf = { TransactionData.class }) +@XmlDiscriminatorValue("CREATE_POLL") public class CreatePollTransactionData extends TransactionData { + + @Schema(description = "Poll creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") + private byte[] pollCreatorPublicKey; + // Properties private String owner; private String pollName; @@ -29,10 +36,15 @@ public class CreatePollTransactionData extends TransactionData { super(TransactionType.CREATE_POLL); } + public void afterUnmarshal(Unmarshaller u, Object parent) { + this.creatorPublicKey = this.pollCreatorPublicKey; + } + public CreatePollTransactionData(BaseTransactionData baseTransactionData, String owner, String pollName, String description, List pollOptions) { super(Transaction.TransactionType.CREATE_POLL, baseTransactionData); + this.creatorPublicKey = baseTransactionData.creatorPublicKey; this.owner = owner; this.pollName = pollName; this.description = description; @@ -41,6 +53,7 @@ public class CreatePollTransactionData extends TransactionData { // Getters/setters + public byte[] getPollCreatorPublicKey() { return this.creatorPublicKey; } public String getOwner() { return this.owner; } diff --git a/src/main/java/org/qortal/data/transaction/TransactionData.java b/src/main/java/org/qortal/data/transaction/TransactionData.java index ec1139f4..838cffd3 100644 --- a/src/main/java/org/qortal/data/transaction/TransactionData.java +++ b/src/main/java/org/qortal/data/transaction/TransactionData.java @@ -12,6 +12,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.transaction.Transaction.ApprovalStatus; import org.qortal.transaction.Transaction.TransactionType; @@ -29,6 +30,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, IssueAssetTransactionData.class, TransferAssetTransactionData.class, CreateAssetOrderTransactionData.class, CancelAssetOrderTransactionData.class, MultiPaymentTransactionData.class, DeployAtTransactionData.class, MessageTransactionData.class, ATTransactionData.class, diff --git a/src/main/java/org/qortal/data/transaction/VoteOnPollTransactionData.java b/src/main/java/org/qortal/data/transaction/VoteOnPollTransactionData.java index 6145d741..a23d5e2b 100644 --- a/src/main/java/org/qortal/data/transaction/VoteOnPollTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/VoteOnPollTransactionData.java @@ -3,7 +3,9 @@ package org.qortal.data.transaction; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlTransient; +import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue; import org.qortal.transaction.Transaction.TransactionType; import io.swagger.v3.oas.annotations.media.Schema; @@ -11,12 +13,17 @@ import io.swagger.v3.oas.annotations.media.Schema; // All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) @Schema(allOf = { TransactionData.class }) +@XmlDiscriminatorValue("VOTE_ON_POLL") public class VoteOnPollTransactionData extends TransactionData { // Properties + @Schema(description = "Vote creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] voterPublicKey; private String pollName; private int optionIndex; + // For internal use when orphaning + @XmlTransient + @Schema(hidden = true) private Integer previousOptionIndex; // Constructors diff --git a/src/main/java/org/qortal/data/voting/PollData.java b/src/main/java/org/qortal/data/voting/PollData.java index 4af62087..1850ddc7 100644 --- a/src/main/java/org/qortal/data/voting/PollData.java +++ b/src/main/java/org/qortal/data/voting/PollData.java @@ -14,6 +14,11 @@ public class PollData { // Constructors + // For JAXB + protected PollData() { + super(); + } + public PollData(byte[] creatorPublicKey, String owner, String pollName, String description, List pollOptions, long published) { this.creatorPublicKey = creatorPublicKey; this.owner = owner; @@ -29,22 +34,42 @@ public class PollData { return this.creatorPublicKey; } + public void setCreatorPublicKey(byte[] creatorPublicKey) { + this.creatorPublicKey = creatorPublicKey; + } + public String getOwner() { return this.owner; } + public void setOwner(String owner) { + this.owner = owner; + } + public String getPollName() { return this.pollName; } + public void setPollName(String pollName) { + this.pollName = pollName; + } + public String getDescription() { return this.description; } + public void setDescription(String description) { + this.description = description; + } + public List getPollOptions() { return this.pollOptions; } + public void setPollOptions(List pollOptions) { + this.pollOptions = pollOptions; + } + public long getPublished() { return this.published; } diff --git a/src/main/java/org/qortal/repository/VotingRepository.java b/src/main/java/org/qortal/repository/VotingRepository.java index 28a9f6c7..b0e2954c 100644 --- a/src/main/java/org/qortal/repository/VotingRepository.java +++ b/src/main/java/org/qortal/repository/VotingRepository.java @@ -9,6 +9,8 @@ public interface VotingRepository { // Polls + public List getAllPolls(Integer limit, Integer offset, Boolean reverse) throws DataException; + public PollData fromPollName(String pollName) throws DataException; public boolean pollExists(String pollName) throws DataException; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java index 447fbe4c..cc33426b 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java @@ -21,6 +21,55 @@ public class HSQLDBVotingRepository implements VotingRepository { // Polls + @Override + public List getAllPolls(Integer limit, Integer offset, Boolean reverse) throws DataException { + StringBuilder sql = new StringBuilder(512); + + sql.append("SELECT poll_name, description, creator, owner, published_when FROM Polls ORDER BY poll_name"); + + if (reverse != null && reverse) + sql.append(" DESC"); + + HSQLDBRepository.limitOffsetSql(sql, limit, offset); + + List polls = new ArrayList<>(); + + try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) { + if (resultSet == null) + return polls; + + do { + String pollName = resultSet.getString(1); + String description = resultSet.getString(2); + byte[] creatorPublicKey = resultSet.getBytes(3); + String owner = resultSet.getString(4); + long published = resultSet.getLong(5); + + String optionsSql = "SELECT option_name FROM PollOptions WHERE poll_name = ? ORDER BY option_index ASC"; + try (ResultSet optionsResultSet = this.repository.checkedExecute(optionsSql, pollName)) { + if (optionsResultSet == null) + return null; + + List pollOptions = new ArrayList<>(); + + // NOTE: do-while because checkedExecute() above has already called rs.next() for us + do { + String optionName = optionsResultSet.getString(1); + + pollOptions.add(new PollOptionData(optionName)); + } while (optionsResultSet.next()); + + polls.add(new PollData(creatorPublicKey, owner, pollName, description, pollOptions, published)); + } + + } while (resultSet.next()); + + return polls; + } catch (SQLException e) { + throw new DataException("Unable to fetch polls from repository", e); + } + } + @Override public PollData fromPollName(String pollName) throws DataException { String sql = "SELECT description, creator, owner, published_when FROM Polls WHERE poll_name = ?";