diff --git a/pom.xml b/pom.xml
index c4e2f65c..2f96e982 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
org.qortal
qortal
- 3.9.0
+ 3.9.1
jar
true
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/ApiService.java b/src/main/java/org/qortal/api/ApiService.java
index 79bfd216..059b8971 100644
--- a/src/main/java/org/qortal/api/ApiService.java
+++ b/src/main/java/org/qortal/api/ApiService.java
@@ -41,6 +41,7 @@ import org.glassfish.jersey.servlet.ServletContainer;
import org.qortal.api.resource.AnnotationPostProcessor;
import org.qortal.api.resource.ApiDefinition;
import org.qortal.api.websocket.*;
+import org.qortal.network.Network;
import org.qortal.settings.Settings;
public class ApiService {
@@ -125,13 +126,13 @@ public class ApiService {
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
new DetectorConnectionFactory(sslConnectionFactory),
httpConnectionFactory);
- portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
+ portUnifiedConnector.setHost(Network.getInstance().getBindAddress());
portUnifiedConnector.setPort(Settings.getInstance().getApiPort());
this.server.addConnector(portUnifiedConnector);
} else {
// Non-SSL
- InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
+ InetAddress bindAddr = InetAddress.getByName(Network.getInstance().getBindAddress());
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getApiPort());
this.server = new Server(endpoint);
}
diff --git a/src/main/java/org/qortal/api/DomainMapService.java b/src/main/java/org/qortal/api/DomainMapService.java
index f5eb8105..3b81d94c 100644
--- a/src/main/java/org/qortal/api/DomainMapService.java
+++ b/src/main/java/org/qortal/api/DomainMapService.java
@@ -15,6 +15,7 @@ import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.qortal.api.resource.AnnotationPostProcessor;
import org.qortal.api.resource.ApiDefinition;
+import org.qortal.network.Network;
import org.qortal.settings.Settings;
import javax.net.ssl.KeyManagerFactory;
@@ -98,13 +99,13 @@ public class DomainMapService {
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
new DetectorConnectionFactory(sslConnectionFactory),
httpConnectionFactory);
- portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
+ portUnifiedConnector.setHost(Network.getInstance().getBindAddress());
portUnifiedConnector.setPort(Settings.getInstance().getDomainMapPort());
this.server.addConnector(portUnifiedConnector);
} else {
// Non-SSL
- InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
+ InetAddress bindAddr = InetAddress.getByName(Network.getInstance().getBindAddress());
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getDomainMapPort());
this.server = new Server(endpoint);
}
diff --git a/src/main/java/org/qortal/api/GatewayService.java b/src/main/java/org/qortal/api/GatewayService.java
index cebec61b..51191af3 100644
--- a/src/main/java/org/qortal/api/GatewayService.java
+++ b/src/main/java/org/qortal/api/GatewayService.java
@@ -15,6 +15,7 @@ import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.qortal.api.resource.AnnotationPostProcessor;
import org.qortal.api.resource.ApiDefinition;
+import org.qortal.network.Network;
import org.qortal.settings.Settings;
import javax.net.ssl.KeyManagerFactory;
@@ -98,13 +99,13 @@ public class GatewayService {
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
new DetectorConnectionFactory(sslConnectionFactory),
httpConnectionFactory);
- portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
+ portUnifiedConnector.setHost(Network.getInstance().getBindAddress());
portUnifiedConnector.setPort(Settings.getInstance().getGatewayPort());
this.server.addConnector(portUnifiedConnector);
} else {
// Non-SSL
- InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
+ InetAddress bindAddr = InetAddress.getByName(Network.getInstance().getBindAddress());
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getGatewayPort());
this.server = new Server(endpoint);
}
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/api/restricted/resource/AdminResource.java b/src/main/java/org/qortal/api/restricted/resource/AdminResource.java
index 77f44343..7d8e95b8 100644
--- a/src/main/java/org/qortal/api/restricted/resource/AdminResource.java
+++ b/src/main/java/org/qortal/api/restricted/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/controller/repository/NamesDatabaseIntegrityCheck.java b/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java
index 004fa692..99eaf105 100644
--- a/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java
+++ b/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java
@@ -13,7 +13,9 @@ import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.utils.Unicode;
+import java.math.BigInteger;
import java.util.*;
+import java.util.stream.Collectors;
public class NamesDatabaseIntegrityCheck {
@@ -28,16 +30,8 @@ public class NamesDatabaseIntegrityCheck {
private List nameTransactions = new ArrayList<>();
+
public int rebuildName(String name, Repository repository) {
- return this.rebuildName(name, repository, null);
- }
-
- public int rebuildName(String name, Repository repository, List referenceNames) {
- // "referenceNames" tracks the linked names that have already been rebuilt, to prevent circular dependencies
- if (referenceNames == null) {
- referenceNames = new ArrayList<>();
- }
-
int modificationCount = 0;
try {
List transactions = this.fetchAllTransactionsInvolvingName(name, repository);
@@ -46,6 +40,14 @@ public class NamesDatabaseIntegrityCheck {
return modificationCount;
}
+ // If this name has been updated at any point, we need to add transactions from the other names to the sequence
+ int added = this.addAdditionalTransactionsRelatingToName(transactions, name, repository);
+ while (added > 0) {
+ // Keep going until all have been added
+ LOGGER.trace("{} added for {}. Looking for more transactions...", added, name);
+ added = this.addAdditionalTransactionsRelatingToName(transactions, name, repository);
+ }
+
// Loop through each past transaction and re-apply it to the Names table
for (TransactionData currentTransaction : transactions) {
@@ -61,29 +63,14 @@ public class NamesDatabaseIntegrityCheck {
// Process UPDATE_NAME transactions
if (currentTransaction.getType() == TransactionType.UPDATE_NAME) {
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) currentTransaction;
-
- if (Objects.equals(updateNameTransactionData.getNewName(), name) &&
- !Objects.equals(updateNameTransactionData.getName(), updateNameTransactionData.getNewName())) {
- // This renames an existing name, so we need to process that instead
-
- if (!referenceNames.contains(name)) {
- referenceNames.add(name);
- this.rebuildName(updateNameTransactionData.getName(), repository, referenceNames);
- }
- else {
- // We've already processed this name so there's nothing more to do
- }
- }
- else {
- Name nameObj = new Name(repository, name);
- if (nameObj != null && nameObj.getNameData() != null) {
- nameObj.update(updateNameTransactionData);
- modificationCount++;
- LOGGER.trace("Processed UPDATE_NAME transaction for name {}", name);
- } else {
- // Something went wrong
- throw new DataException(String.format("Name data not found for name %s", updateNameTransactionData.getName()));
- }
+ Name nameObj = new Name(repository, updateNameTransactionData.getName());
+ if (nameObj != null && nameObj.getNameData() != null) {
+ nameObj.update(updateNameTransactionData);
+ modificationCount++;
+ LOGGER.trace("Processed UPDATE_NAME transaction for name {}", name);
+ } else {
+ // Something went wrong
+ throw new DataException(String.format("Name data not found for name %s", updateNameTransactionData.getName()));
}
}
@@ -354,8 +341,8 @@ public class NamesDatabaseIntegrityCheck {
}
}
- // Sort by lowest timestamp first
- transactions.sort(Comparator.comparingLong(TransactionData::getTimestamp));
+ // Sort by lowest block height first
+ sortTransactions(transactions);
return transactions;
}
@@ -419,4 +406,67 @@ public class NamesDatabaseIntegrityCheck {
return names;
}
+ private int addAdditionalTransactionsRelatingToName(List transactions, String name, Repository repository) throws DataException {
+ int added = 0;
+
+ // If this name has been updated at any point, we need to add transactions from the other names to the sequence
+ List otherNames = new ArrayList<>();
+ List updateNameTransactions = transactions.stream().filter(t -> t.getType() == TransactionType.UPDATE_NAME).collect(Collectors.toList());
+ for (TransactionData transactionData : updateNameTransactions) {
+ UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
+ // If the newName field isn't empty, and either the "name" or "newName" is different from our reference name,
+ // we should remember this additional name, in case it has relevant transactions associated with it.
+ if (updateNameTransactionData.getNewName() != null && !updateNameTransactionData.getNewName().isEmpty()) {
+ if (!Objects.equals(updateNameTransactionData.getName(), name)) {
+ otherNames.add(updateNameTransactionData.getName());
+ }
+ if (!Objects.equals(updateNameTransactionData.getNewName(), name)) {
+ otherNames.add(updateNameTransactionData.getNewName());
+ }
+ }
+ }
+
+
+ for (String otherName : otherNames) {
+ List otherNameTransactions = this.fetchAllTransactionsInvolvingName(otherName, repository);
+ for (TransactionData otherNameTransactionData : otherNameTransactions) {
+ if (!transactions.contains(otherNameTransactionData)) {
+ // Add new transaction relating to other name
+ transactions.add(otherNameTransactionData);
+ added++;
+ }
+ }
+ }
+
+ if (added > 0) {
+ // New transaction(s) added, so re-sort
+ sortTransactions(transactions);
+ }
+
+ return added;
+ }
+
+ private void sortTransactions(List transactions) {
+ Collections.sort(transactions, new Comparator() {
+ public int compare(Object o1, Object o2) {
+ TransactionData td1 = (TransactionData) o1;
+ TransactionData td2 = (TransactionData) o2;
+
+ // Sort by block height first
+ int heightComparison = td1.getBlockHeight().compareTo(td2.getBlockHeight());
+ if (heightComparison != 0) {
+ return heightComparison;
+ }
+
+ // Same height so compare timestamps
+ int timestampComparison = Long.compare(td1.getTimestamp(), td2.getTimestamp());
+ if (timestampComparison != 0) {
+ return timestampComparison;
+ }
+
+ // Same timestamp so compare signatures
+ return new BigInteger(td1.getSignature()).compareTo(new BigInteger(td2.getSignature()));
+ }});
+ }
+
}
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/network/Network.java b/src/main/java/org/qortal/network/Network.java
index f8f73c2a..ca79f367 100644
--- a/src/main/java/org/qortal/network/Network.java
+++ b/src/main/java/org/qortal/network/Network.java
@@ -124,6 +124,8 @@ public class Network {
private final List selfPeers = new ArrayList<>();
+ private String bindAddress = null;
+
private final ExecuteProduceConsume networkEPC;
private Selector channelSelector;
private ServerSocketChannel serverChannel;
@@ -159,25 +161,43 @@ public class Network {
// Grab P2P port from settings
int listenPort = Settings.getInstance().getListenPort();
- // Grab P2P bind address from settings
- try {
- InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
- InetSocketAddress endpoint = new InetSocketAddress(bindAddr, listenPort);
+ // Grab P2P bind addresses from settings
+ List bindAddresses = new ArrayList<>();
+ if (Settings.getInstance().getBindAddress() != null) {
+ bindAddresses.add(Settings.getInstance().getBindAddress());
+ }
+ if (Settings.getInstance().getBindAddressFallback() != null) {
+ bindAddresses.add(Settings.getInstance().getBindAddressFallback());
+ }
- channelSelector = Selector.open();
+ for (int i=0; i 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 = ?";
diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java
index e2fd1200..6b703bea 100644
--- a/src/main/java/org/qortal/settings/Settings.java
+++ b/src/main/java/org/qortal/settings/Settings.java
@@ -61,6 +61,7 @@ public class Settings {
// Common to all networking (API/P2P)
private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses
+ private String bindAddressFallback = "0.0.0.0"; // Some systems are unable to bind using IPv6
// UI servers
private int uiPort = 12388;
@@ -689,6 +690,10 @@ public class Settings {
return this.bindAddress;
}
+ public String getBindAddressFallback() {
+ return this.bindAddressFallback;
+ }
+
public boolean isUPnPEnabled() {
return this.uPnPEnabled;
}
diff --git a/src/test/java/org/qortal/test/naming/IntegrityTests.java b/src/test/java/org/qortal/test/naming/IntegrityTests.java
index d52d4983..767ea388 100644
--- a/src/test/java/org/qortal/test/naming/IntegrityTests.java
+++ b/src/test/java/org/qortal/test/naming/IntegrityTests.java
@@ -128,7 +128,7 @@ public class IntegrityTests extends Common {
// Run the database integrity check for the initial name, to ensure it doesn't get into a loop
NamesDatabaseIntegrityCheck integrityCheck = new NamesDatabaseIntegrityCheck();
- assertEquals(2, integrityCheck.rebuildName(initialName, repository));
+ assertEquals(4, integrityCheck.rebuildName(initialName, repository)); // 4 transactions total
// Ensure the new name still exists and the data is still correct
assertTrue(repository.getNameRepository().nameExists(initialName));