forked from Qortal/qortal
Added Vote-on-poll transaction
* Added simple genesis block timestamp setting to help testing * Fixed setting account's last reference * Moved some Poll constants from CreatePollTransaction to Poll * HSQLDB: added TYPE PollOptionIndex * HSQLDB: added previous_option_index to VoteOnPollTransactions table to help orphaning * HSQLDB: renamed "poll" to "poll_name" in same table * HSQLDB: PollOptions now has additional option_index column * Improved TransactionTests to allow for different genesis block timestamps. * Also added VoteOnPollTransaction test
This commit is contained in:
parent
9651192a2d
commit
fe6cb4e366
57
src/data/transaction/VoteOnPollTransactionData.java
Normal file
57
src/data/transaction/VoteOnPollTransactionData.java
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package data.transaction;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import qora.transaction.Transaction.TransactionType;
|
||||||
|
|
||||||
|
public class VoteOnPollTransactionData extends TransactionData {
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
private byte[] voterPublicKey;
|
||||||
|
private String pollName;
|
||||||
|
private int optionIndex;
|
||||||
|
private Integer previousOptionIndex;
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
public VoteOnPollTransactionData(byte[] voterPublicKey, String pollName, int optionIndex, Integer previousOptionIndex, BigDecimal fee, long timestamp,
|
||||||
|
byte[] reference, byte[] signature) {
|
||||||
|
super(TransactionType.VOTE_ON_POLL, fee, voterPublicKey, timestamp, reference, signature);
|
||||||
|
|
||||||
|
this.voterPublicKey = voterPublicKey;
|
||||||
|
this.pollName = pollName;
|
||||||
|
this.optionIndex = optionIndex;
|
||||||
|
this.previousOptionIndex = previousOptionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VoteOnPollTransactionData(byte[] voterPublicKey, String pollName, int optionIndex, BigDecimal fee, long timestamp, byte[] reference,
|
||||||
|
byte[] signature) {
|
||||||
|
this(voterPublicKey, pollName, optionIndex, null, fee, timestamp, reference, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
public VoteOnPollTransactionData(byte[] voterPublicKey, String pollName, int optionIndex, BigDecimal fee, long timestamp, byte[] reference) {
|
||||||
|
this(voterPublicKey, pollName, optionIndex, null, fee, timestamp, reference, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters / setters
|
||||||
|
|
||||||
|
public byte[] getVoterPublicKey() {
|
||||||
|
return this.voterPublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPollName() {
|
||||||
|
return this.pollName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOptionIndex() {
|
||||||
|
return this.optionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getPreviousOptionIndex() {
|
||||||
|
return this.previousOptionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPreviousOptionIndex(Integer previousOptionIndex) {
|
||||||
|
this.previousOptionIndex = previousOptionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
src/data/voting/VoteOnPollData.java
Normal file
32
src/data/voting/VoteOnPollData.java
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package data.voting;
|
||||||
|
|
||||||
|
public class VoteOnPollData {
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
private String pollName;
|
||||||
|
private byte[] voterPublicKey;
|
||||||
|
private int optionIndex;
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
public VoteOnPollData(String pollName, byte[] voterPublicKey, int optionIndex) {
|
||||||
|
this.pollName = pollName;
|
||||||
|
this.voterPublicKey = voterPublicKey;
|
||||||
|
this.optionIndex = optionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters/setters
|
||||||
|
|
||||||
|
public String getPollName() {
|
||||||
|
return this.pollName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getVoterPublicKey() {
|
||||||
|
return this.voterPublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOptionIndex() {
|
||||||
|
return this.optionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -158,6 +158,8 @@ public class Account {
|
|||||||
* @throws DataException
|
* @throws DataException
|
||||||
*/
|
*/
|
||||||
public void setLastReference(byte[] reference) throws DataException {
|
public void setLastReference(byte[] reference) throws DataException {
|
||||||
|
accountData.setReference(reference);
|
||||||
|
|
||||||
this.repository.getAccountRepository().save(accountData);
|
this.repository.getAccountRepository().save(accountData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,22 +17,24 @@ import qora.crypto.Crypto;
|
|||||||
import qora.transaction.Transaction;
|
import qora.transaction.Transaction;
|
||||||
import repository.DataException;
|
import repository.DataException;
|
||||||
import repository.Repository;
|
import repository.Repository;
|
||||||
import utils.NTP;
|
import settings.Settings;
|
||||||
|
|
||||||
public class GenesisBlock extends Block {
|
public class GenesisBlock extends Block {
|
||||||
|
|
||||||
|
// Properties
|
||||||
private static final int GENESIS_BLOCK_VERSION = 1;
|
private static final int GENESIS_BLOCK_VERSION = 1;
|
||||||
private static final byte[] GENESIS_REFERENCE = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 }; // NOTE: Neither 64 nor 128 bytes!
|
private static final byte[] GENESIS_REFERENCE = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 }; // NOTE: Neither 64 nor 128 bytes!
|
||||||
private static final BigDecimal GENESIS_GENERATING_BALANCE = BigDecimal.valueOf(10_000_000L).setScale(8);
|
private static final BigDecimal GENESIS_GENERATING_BALANCE = BigDecimal.valueOf(10_000_000L).setScale(8);
|
||||||
private static final byte[] GENESIS_GENERATOR_PUBLIC_KEY = GenesisAccount.PUBLIC_KEY; // NOTE: 8 bytes not 32 bytes!
|
private static final byte[] GENESIS_GENERATOR_PUBLIC_KEY = GenesisAccount.PUBLIC_KEY; // NOTE: 8 bytes not 32 bytes!
|
||||||
private static final long GENESIS_TIMESTAMP = 1400247274336L; // QORA RELEASE: Fri May 16 13:34:34.336 2014 UTC
|
public static final long GENESIS_TIMESTAMP = 1400247274336L; // QORA RELEASE: Fri May 16 13:34:34.336 2014 UTC
|
||||||
private static final byte[] GENESIS_GENERATOR_SIGNATURE = calcSignature();
|
private static final byte[] GENESIS_GENERATOR_SIGNATURE = calcSignature();
|
||||||
private static final byte[] GENESIS_TRANSACTIONS_SIGNATURE = calcSignature();
|
private static final byte[] GENESIS_TRANSACTIONS_SIGNATURE = calcSignature();
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
public GenesisBlock(Repository repository) throws DataException {
|
public GenesisBlock(Repository repository) throws DataException {
|
||||||
super(repository, new BlockData(GENESIS_BLOCK_VERSION, GENESIS_REFERENCE, 0, BigDecimal.ZERO.setScale(8), GENESIS_TRANSACTIONS_SIGNATURE, 1,
|
super(repository, new BlockData(GENESIS_BLOCK_VERSION, GENESIS_REFERENCE, 0, BigDecimal.ZERO.setScale(8), GENESIS_TRANSACTIONS_SIGNATURE, 1,
|
||||||
GENESIS_TIMESTAMP, GENESIS_GENERATING_BALANCE, GENESIS_GENERATOR_PUBLIC_KEY, GENESIS_GENERATOR_SIGNATURE, null, null));
|
Settings.getInstance().getGenesisTimestamp(), GENESIS_GENERATING_BALANCE, GENESIS_GENERATOR_PUBLIC_KEY, GENESIS_GENERATOR_SIGNATURE, null, null));
|
||||||
|
|
||||||
this.transactions = new ArrayList<Transaction>();
|
this.transactions = new ArrayList<Transaction>();
|
||||||
|
|
||||||
|
@ -24,11 +24,6 @@ public class CreatePollTransaction extends Transaction {
|
|||||||
// Properties
|
// Properties
|
||||||
private CreatePollTransactionData createPollTransactionData;
|
private CreatePollTransactionData createPollTransactionData;
|
||||||
|
|
||||||
// Other useful constants
|
|
||||||
public static final int MAX_NAME_SIZE = 400;
|
|
||||||
public static final int MAX_DESCRIPTION_SIZE = 4000;
|
|
||||||
public static final int MAX_OPTIONS = 100;
|
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
public CreatePollTransaction(Repository repository, TransactionData transactionData) {
|
public CreatePollTransaction(Repository repository, TransactionData transactionData) {
|
||||||
@ -89,12 +84,12 @@ public class CreatePollTransaction extends Transaction {
|
|||||||
return ValidationResult.INVALID_ADDRESS;
|
return ValidationResult.INVALID_ADDRESS;
|
||||||
|
|
||||||
// Check name size bounds
|
// Check name size bounds
|
||||||
if (createPollTransactionData.getPollName().length() < 1 || createPollTransactionData.getPollName().length() > CreatePollTransaction.MAX_NAME_SIZE)
|
if (createPollTransactionData.getPollName().length() < 1 || createPollTransactionData.getPollName().length() > Poll.MAX_NAME_SIZE)
|
||||||
return ValidationResult.INVALID_NAME_LENGTH;
|
return ValidationResult.INVALID_NAME_LENGTH;
|
||||||
|
|
||||||
// Check description size bounds
|
// Check description size bounds
|
||||||
if (createPollTransactionData.getDescription().length() < 1
|
if (createPollTransactionData.getDescription().length() < 1
|
||||||
|| createPollTransactionData.getDescription().length() > CreatePollTransaction.MAX_DESCRIPTION_SIZE)
|
|| createPollTransactionData.getDescription().length() > Poll.MAX_DESCRIPTION_SIZE)
|
||||||
return ValidationResult.INVALID_DESCRIPTION_LENGTH;
|
return ValidationResult.INVALID_DESCRIPTION_LENGTH;
|
||||||
|
|
||||||
// Check poll name is lowercase
|
// Check poll name is lowercase
|
||||||
@ -110,7 +105,7 @@ public class CreatePollTransaction extends Transaction {
|
|||||||
// Check number of options
|
// Check number of options
|
||||||
List<PollOptionData> pollOptions = createPollTransactionData.getPollOptions();
|
List<PollOptionData> pollOptions = createPollTransactionData.getPollOptions();
|
||||||
int pollOptionsCount = pollOptions.size();
|
int pollOptionsCount = pollOptions.size();
|
||||||
if (pollOptionsCount < 1 || pollOptionsCount > MAX_OPTIONS)
|
if (pollOptionsCount < 1 || pollOptionsCount > Poll.MAX_OPTIONS)
|
||||||
return ValidationResult.INVALID_OPTIONS_COUNT;
|
return ValidationResult.INVALID_OPTIONS_COUNT;
|
||||||
|
|
||||||
// Check each option
|
// Check each option
|
||||||
@ -118,7 +113,7 @@ public class CreatePollTransaction extends Transaction {
|
|||||||
for (PollOptionData pollOptionData : pollOptions) {
|
for (PollOptionData pollOptionData : pollOptions) {
|
||||||
// Check option length
|
// Check option length
|
||||||
int optionNameLength = pollOptionData.getOptionName().getBytes(StandardCharsets.UTF_8).length;
|
int optionNameLength = pollOptionData.getOptionName().getBytes(StandardCharsets.UTF_8).length;
|
||||||
if (optionNameLength < 1 || optionNameLength > MAX_NAME_SIZE)
|
if (optionNameLength < 1 || optionNameLength > Poll.MAX_NAME_SIZE)
|
||||||
return ValidationResult.INVALID_OPTION_LENGTH;
|
return ValidationResult.INVALID_OPTION_LENGTH;
|
||||||
|
|
||||||
// Check option is unique. NOTE: NOT case-sensitive!
|
// Check option is unique. NOTE: NOT case-sensitive!
|
||||||
|
@ -45,9 +45,10 @@ public abstract class Transaction {
|
|||||||
public enum ValidationResult {
|
public enum ValidationResult {
|
||||||
OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6), INVALID_NAME_LENGTH(7), INVALID_AMOUNT(
|
OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6), INVALID_NAME_LENGTH(7), INVALID_AMOUNT(
|
||||||
15), NAME_NOT_LOWER_CASE(17), INVALID_DESCRIPTION_LENGTH(18), INVALID_OPTIONS_COUNT(19), INVALID_OPTION_LENGTH(20), DUPLICATE_OPTION(
|
15), NAME_NOT_LOWER_CASE(17), INVALID_DESCRIPTION_LENGTH(18), INVALID_OPTIONS_COUNT(19), INVALID_OPTION_LENGTH(20), DUPLICATE_OPTION(
|
||||||
21), POLL_ALREADY_EXISTS(22), INVALID_DATA_LENGTH(27), INVALID_QUANTITY(28), ASSET_DOES_NOT_EXIST(29), INVALID_RETURN(
|
21), POLL_ALREADY_EXISTS(22), POLL_DOES_NOT_EXIST(24), POLL_OPTION_DOES_NOT_EXIST(25), ALREADY_VOTED_FOR_THAT_OPTION(
|
||||||
30), HAVE_EQUALS_WANT(31), ORDER_DOES_NOT_EXIST(32), INVALID_ORDER_CREATOR(
|
26), INVALID_DATA_LENGTH(27), INVALID_QUANTITY(28), ASSET_DOES_NOT_EXIST(29), INVALID_RETURN(30), HAVE_EQUALS_WANT(
|
||||||
33), INVALID_PAYMENTS_COUNT(34), NEGATIVE_PRICE(35), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000);
|
31), ORDER_DOES_NOT_EXIST(32), INVALID_ORDER_CREATOR(
|
||||||
|
33), INVALID_PAYMENTS_COUNT(34), NEGATIVE_PRICE(35), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000);
|
||||||
|
|
||||||
public final int value;
|
public final int value;
|
||||||
|
|
||||||
@ -106,6 +107,9 @@ public abstract class Transaction {
|
|||||||
case CREATE_POLL:
|
case CREATE_POLL:
|
||||||
return new CreatePollTransaction(repository, transactionData);
|
return new CreatePollTransaction(repository, transactionData);
|
||||||
|
|
||||||
|
case VOTE_ON_POLL:
|
||||||
|
return new VoteOnPollTransaction(repository, transactionData);
|
||||||
|
|
||||||
case ISSUE_ASSET:
|
case ISSUE_ASSET:
|
||||||
return new IssueAssetTransaction(repository, transactionData);
|
return new IssueAssetTransaction(repository, transactionData);
|
||||||
|
|
||||||
|
162
src/qora/transaction/VoteOnPollTransaction.java
Normal file
162
src/qora/transaction/VoteOnPollTransaction.java
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
package qora.transaction;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import data.transaction.TransactionData;
|
||||||
|
import data.transaction.VoteOnPollTransactionData;
|
||||||
|
import data.voting.PollData;
|
||||||
|
import data.voting.PollOptionData;
|
||||||
|
import data.voting.VoteOnPollData;
|
||||||
|
import qora.account.Account;
|
||||||
|
import qora.account.PublicKeyAccount;
|
||||||
|
import qora.assets.Asset;
|
||||||
|
import qora.block.BlockChain;
|
||||||
|
import qora.voting.Poll;
|
||||||
|
import repository.DataException;
|
||||||
|
import repository.Repository;
|
||||||
|
import repository.VotingRepository;
|
||||||
|
|
||||||
|
public class VoteOnPollTransaction extends Transaction {
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
private VoteOnPollTransactionData voteOnPollTransactionData;
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
public VoteOnPollTransaction(Repository repository, TransactionData transactionData) {
|
||||||
|
super(repository, transactionData);
|
||||||
|
|
||||||
|
this.voteOnPollTransactionData = (VoteOnPollTransactionData) this.transactionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// More information
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Account> getRecipientAccounts() {
|
||||||
|
return new ArrayList<Account>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInvolved(Account account) throws DataException {
|
||||||
|
return account.getAddress().equals(this.getCreator().getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal getAmount(Account account) throws DataException {
|
||||||
|
BigDecimal amount = BigDecimal.ZERO.setScale(8);
|
||||||
|
|
||||||
|
if (account.getAddress().equals(this.getCreator().getAddress()))
|
||||||
|
amount = amount.subtract(this.transactionData.getFee());
|
||||||
|
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processing
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationResult isValid() throws DataException {
|
||||||
|
// Are VoteOnPollTransactions even allowed at this point?
|
||||||
|
// XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used?
|
||||||
|
if (this.voteOnPollTransactionData.getTimestamp() < BlockChain.VOTING_RELEASE_TIMESTAMP)
|
||||||
|
return ValidationResult.NOT_YET_RELEASED;
|
||||||
|
|
||||||
|
// Check name size bounds
|
||||||
|
if (voteOnPollTransactionData.getPollName().length() < 1 || voteOnPollTransactionData.getPollName().length() > Poll.MAX_NAME_SIZE)
|
||||||
|
return ValidationResult.INVALID_NAME_LENGTH;
|
||||||
|
|
||||||
|
// Check poll name is lowercase
|
||||||
|
if (!voteOnPollTransactionData.getPollName().equals(voteOnPollTransactionData.getPollName().toLowerCase()))
|
||||||
|
return ValidationResult.NAME_NOT_LOWER_CASE;
|
||||||
|
|
||||||
|
VotingRepository votingRepository = this.repository.getVotingRepository();
|
||||||
|
|
||||||
|
// Check poll exists
|
||||||
|
PollData pollData = votingRepository.fromPollName(voteOnPollTransactionData.getPollName());
|
||||||
|
if (pollData == null)
|
||||||
|
return ValidationResult.POLL_DOES_NOT_EXIST;
|
||||||
|
|
||||||
|
// Check poll option index is within bounds
|
||||||
|
List<PollOptionData> pollOptions = pollData.getPollOptions();
|
||||||
|
int optionIndex = voteOnPollTransactionData.getOptionIndex();
|
||||||
|
|
||||||
|
if (optionIndex < 0 || optionIndex > pollOptions.size() - 1)
|
||||||
|
return ValidationResult.POLL_OPTION_DOES_NOT_EXIST;
|
||||||
|
|
||||||
|
// Check if vote already exists
|
||||||
|
VoteOnPollData voteOnPollData = votingRepository.getVote(voteOnPollTransactionData.getPollName(), voteOnPollTransactionData.getVoterPublicKey());
|
||||||
|
if (voteOnPollData != null && voteOnPollData.getOptionIndex() == optionIndex)
|
||||||
|
return ValidationResult.ALREADY_VOTED_FOR_THAT_OPTION;
|
||||||
|
|
||||||
|
// Check fee is positive
|
||||||
|
if (voteOnPollTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0)
|
||||||
|
return ValidationResult.NEGATIVE_FEE;
|
||||||
|
|
||||||
|
// Check reference is correct
|
||||||
|
PublicKeyAccount creator = new PublicKeyAccount(this.repository, voteOnPollTransactionData.getCreatorPublicKey());
|
||||||
|
|
||||||
|
if (!Arrays.equals(creator.getLastReference(), voteOnPollTransactionData.getReference()))
|
||||||
|
return ValidationResult.INVALID_REFERENCE;
|
||||||
|
|
||||||
|
// Check issuer has enough funds
|
||||||
|
if (creator.getConfirmedBalance(Asset.QORA).compareTo(voteOnPollTransactionData.getFee()) == -1)
|
||||||
|
return ValidationResult.NO_BALANCE;
|
||||||
|
|
||||||
|
return ValidationResult.OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void process() throws DataException {
|
||||||
|
// Update voter's balance
|
||||||
|
Account voter = new PublicKeyAccount(this.repository, voteOnPollTransactionData.getVoterPublicKey());
|
||||||
|
voter.setConfirmedBalance(Asset.QORA, voter.getConfirmedBalance(Asset.QORA).subtract(voteOnPollTransactionData.getFee()));
|
||||||
|
|
||||||
|
// Update vote's reference
|
||||||
|
voter.setLastReference(voteOnPollTransactionData.getSignature());
|
||||||
|
|
||||||
|
VotingRepository votingRepository = this.repository.getVotingRepository();
|
||||||
|
|
||||||
|
// Check for previous vote so we can save option in case of orphaning
|
||||||
|
VoteOnPollData previousVoteOnPollData = votingRepository.getVote(voteOnPollTransactionData.getPollName(),
|
||||||
|
voteOnPollTransactionData.getVoterPublicKey());
|
||||||
|
if (previousVoteOnPollData != null)
|
||||||
|
voteOnPollTransactionData.setPreviousOptionIndex(previousVoteOnPollData.getOptionIndex());
|
||||||
|
|
||||||
|
// Save this transaction, now with possible previous vote
|
||||||
|
this.repository.getTransactionRepository().save(voteOnPollTransactionData);
|
||||||
|
|
||||||
|
// Apply vote to poll
|
||||||
|
VoteOnPollData newVoteOnPollData = new VoteOnPollData(voteOnPollTransactionData.getPollName(), voteOnPollTransactionData.getVoterPublicKey(),
|
||||||
|
voteOnPollTransactionData.getOptionIndex());
|
||||||
|
votingRepository.save(newVoteOnPollData);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void orphan() throws DataException {
|
||||||
|
// Update issuer's balance
|
||||||
|
Account voter = new PublicKeyAccount(this.repository, voteOnPollTransactionData.getVoterPublicKey());
|
||||||
|
voter.setConfirmedBalance(Asset.QORA, voter.getConfirmedBalance(Asset.QORA).add(voteOnPollTransactionData.getFee()));
|
||||||
|
|
||||||
|
// Update issuer's reference
|
||||||
|
voter.setLastReference(voteOnPollTransactionData.getReference());
|
||||||
|
|
||||||
|
// Does this transaction have previous vote info?
|
||||||
|
VotingRepository votingRepository = this.repository.getVotingRepository();
|
||||||
|
Integer previousOptionIndex = voteOnPollTransactionData.getPreviousOptionIndex();
|
||||||
|
if (previousOptionIndex != null) {
|
||||||
|
// Reinstate previous vote
|
||||||
|
VoteOnPollData previousVoteOnPollData = new VoteOnPollData(voteOnPollTransactionData.getPollName(), voteOnPollTransactionData.getVoterPublicKey(),
|
||||||
|
previousOptionIndex);
|
||||||
|
votingRepository.save(previousVoteOnPollData);
|
||||||
|
} else {
|
||||||
|
// Delete vote
|
||||||
|
votingRepository.delete(voteOnPollTransactionData.getPollName(), voteOnPollTransactionData.getVoterPublicKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete this transaction itself
|
||||||
|
this.repository.getTransactionRepository().delete(voteOnPollTransactionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -11,6 +11,11 @@ public class Poll {
|
|||||||
private Repository repository;
|
private Repository repository;
|
||||||
private PollData pollData;
|
private PollData pollData;
|
||||||
|
|
||||||
|
// Other useful constants
|
||||||
|
public static final int MAX_NAME_SIZE = 400;
|
||||||
|
public static final int MAX_DESCRIPTION_SIZE = 4000;
|
||||||
|
public static final int MAX_OPTIONS = 100;
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package repository;
|
package repository;
|
||||||
|
|
||||||
import data.voting.PollData;
|
import data.voting.PollData;
|
||||||
|
import data.voting.VoteOnPollData;
|
||||||
|
|
||||||
public interface VotingRepository {
|
public interface VotingRepository {
|
||||||
|
|
||||||
|
// Polls
|
||||||
|
|
||||||
public PollData fromPollName(String pollName) throws DataException;
|
public PollData fromPollName(String pollName) throws DataException;
|
||||||
|
|
||||||
public boolean pollExists(String pollName) throws DataException;
|
public boolean pollExists(String pollName) throws DataException;
|
||||||
@ -12,4 +15,12 @@ public interface VotingRepository {
|
|||||||
|
|
||||||
public void delete(String pollName) throws DataException;
|
public void delete(String pollName) throws DataException;
|
||||||
|
|
||||||
|
// Votes
|
||||||
|
|
||||||
|
public VoteOnPollData getVote(String pollName, byte[] voterPublicKey) throws DataException;
|
||||||
|
|
||||||
|
public void save(VoteOnPollData voteOnPollData) throws DataException;
|
||||||
|
|
||||||
|
public void delete(String pollName, byte[] voterPublicKey) throws DataException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -88,6 +88,7 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
stmt.execute("CREATE TYPE NameData AS VARCHAR(4000)");
|
stmt.execute("CREATE TYPE NameData AS VARCHAR(4000)");
|
||||||
stmt.execute("CREATE TYPE PollName AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
|
stmt.execute("CREATE TYPE PollName AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
|
||||||
stmt.execute("CREATE TYPE PollOption AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
|
stmt.execute("CREATE TYPE PollOption AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
|
||||||
|
stmt.execute("CREATE TYPE PollOptionIndex AS INTEGER");
|
||||||
stmt.execute("CREATE TYPE DataHash AS VARCHAR(100)");
|
stmt.execute("CREATE TYPE DataHash AS VARCHAR(100)");
|
||||||
stmt.execute("CREATE TYPE AssetID AS BIGINT");
|
stmt.execute("CREATE TYPE AssetID AS BIGINT");
|
||||||
stmt.execute("CREATE TYPE AssetName AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
|
stmt.execute("CREATE TYPE AssetName AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
|
||||||
@ -206,8 +207,8 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
|
|
||||||
case 11:
|
case 11:
|
||||||
// Vote On Poll Transactions
|
// Vote On Poll Transactions
|
||||||
stmt.execute("CREATE TABLE VoteOnPollTransactions (signature Signature, voter QoraPublicKey NOT NULL, poll PollName NOT NULL, "
|
stmt.execute("CREATE TABLE VoteOnPollTransactions (signature Signature, voter QoraPublicKey NOT NULL, poll_name PollName NOT NULL, "
|
||||||
+ "option_index INTEGER NOT NULL, "
|
+ "option_index PollOptionIndex NOT NULL, previous_option_index PollOptionIndex, "
|
||||||
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
|
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -314,10 +315,10 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
"CREATE TABLE Polls (poll_name PollName, description VARCHAR(4000) NOT NULL, creator QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, "
|
"CREATE TABLE Polls (poll_name PollName, description VARCHAR(4000) NOT NULL, creator QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, "
|
||||||
+ "published TIMESTAMP NOT NULL, " + "PRIMARY KEY (poll_name))");
|
+ "published TIMESTAMP NOT NULL, " + "PRIMARY KEY (poll_name))");
|
||||||
// Various options available on a poll
|
// Various options available on a poll
|
||||||
stmt.execute("CREATE TABLE PollOptions (poll_name PollName, option_name PollOption, "
|
stmt.execute("CREATE TABLE PollOptions (poll_name PollName, option_index TINYINT NOT NULL, option_name PollOption, "
|
||||||
+ "PRIMARY KEY (poll_name, option_name), FOREIGN KEY (poll_name) REFERENCES Polls (poll_name) ON DELETE CASCADE)");
|
+ "PRIMARY KEY (poll_name, option_index), FOREIGN KEY (poll_name) REFERENCES Polls (poll_name) ON DELETE CASCADE)");
|
||||||
// Actual votes cast on a poll by voting users. NOTE: only one vote per user supported at this time.
|
// Actual votes cast on a poll by voting users. NOTE: only one vote per user supported at this time.
|
||||||
stmt.execute("CREATE TABLE PollVotes (poll_name PollName, voter QoraPublicKey, option_name PollOption, "
|
stmt.execute("CREATE TABLE PollVotes (poll_name PollName, voter QoraPublicKey, option_index PollOptionIndex NOT NULL, "
|
||||||
+ "PRIMARY KEY (poll_name, voter), FOREIGN KEY (poll_name) REFERENCES Polls (poll_name) ON DELETE CASCADE)");
|
+ "PRIMARY KEY (poll_name, voter), FOREIGN KEY (poll_name) REFERENCES Polls (poll_name) ON DELETE CASCADE)");
|
||||||
// For when a user wants to lookup poll they own
|
// For when a user wants to lookup poll they own
|
||||||
stmt.execute("CREATE INDEX PollOwnerIndex on Polls (owner)");
|
stmt.execute("CREATE INDEX PollOwnerIndex on Polls (owner)");
|
||||||
|
@ -8,6 +8,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import data.voting.PollData;
|
import data.voting.PollData;
|
||||||
import data.voting.PollOptionData;
|
import data.voting.PollOptionData;
|
||||||
|
import data.voting.VoteOnPollData;
|
||||||
import repository.VotingRepository;
|
import repository.VotingRepository;
|
||||||
import repository.DataException;
|
import repository.DataException;
|
||||||
|
|
||||||
@ -19,6 +20,8 @@ public class HSQLDBVotingRepository implements VotingRepository {
|
|||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Votes
|
||||||
|
|
||||||
public PollData fromPollName(String pollName) throws DataException {
|
public PollData fromPollName(String pollName) throws DataException {
|
||||||
try {
|
try {
|
||||||
ResultSet resultSet = this.repository.checkedExecute("SELECT description, creator, owner, published FROM Polls WHERE poll_name = ?", pollName);
|
ResultSet resultSet = this.repository.checkedExecute("SELECT description, creator, owner, published FROM Polls WHERE poll_name = ?", pollName);
|
||||||
@ -30,7 +33,7 @@ public class HSQLDBVotingRepository implements VotingRepository {
|
|||||||
String owner = resultSet.getString(3);
|
String owner = resultSet.getString(3);
|
||||||
long published = resultSet.getTimestamp(4).getTime();
|
long published = resultSet.getTimestamp(4).getTime();
|
||||||
|
|
||||||
resultSet = this.repository.checkedExecute("SELECT option_name FROM PollOptions where poll_name = ?", pollName);
|
resultSet = this.repository.checkedExecute("SELECT option_name FROM PollOptions where poll_name = ? ORDER BY option_index ASC", pollName);
|
||||||
if (resultSet == null)
|
if (resultSet == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@ -70,10 +73,13 @@ public class HSQLDBVotingRepository implements VotingRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now attempt to save poll options
|
// Now attempt to save poll options
|
||||||
for (PollOptionData pollOptionData : pollData.getPollOptions()) {
|
List<PollOptionData> pollOptions = pollData.getPollOptions();
|
||||||
|
for (int optionIndex = 0; optionIndex < pollOptions.size(); ++optionIndex) {
|
||||||
|
PollOptionData pollOptionData = pollOptions.get(optionIndex);
|
||||||
|
|
||||||
HSQLDBSaver optionSaveHelper = new HSQLDBSaver("PollOptions");
|
HSQLDBSaver optionSaveHelper = new HSQLDBSaver("PollOptions");
|
||||||
|
|
||||||
optionSaveHelper.bind("poll_name", pollData.getPollName()).bind("option_name", pollOptionData.getOptionName());
|
optionSaveHelper.bind("poll_name", pollData.getPollName()).bind("option_index", optionIndex).bind("option_name", pollOptionData.getOptionName());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
optionSaveHelper.execute(this.repository);
|
optionSaveHelper.execute(this.repository);
|
||||||
@ -93,4 +99,42 @@ public class HSQLDBVotingRepository implements VotingRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Votes
|
||||||
|
|
||||||
|
public VoteOnPollData getVote(String pollName, byte[] voterPublicKey) throws DataException {
|
||||||
|
try {
|
||||||
|
ResultSet resultSet = this.repository.checkedExecute("SELECT option_index FROM PollVotes WHERE poll_name = ? AND voter = ?", pollName,
|
||||||
|
voterPublicKey);
|
||||||
|
if (resultSet == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int optionIndex = resultSet.getInt(1);
|
||||||
|
|
||||||
|
return new VoteOnPollData(pollName, voterPublicKey, optionIndex);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch poll vote from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(VoteOnPollData voteOnPollData) throws DataException {
|
||||||
|
HSQLDBSaver saveHelper = new HSQLDBSaver("PollVotes");
|
||||||
|
|
||||||
|
saveHelper.bind("poll_name", voteOnPollData.getPollName()).bind("voter", voteOnPollData.getVoterPublicKey()).bind("option_index",
|
||||||
|
voteOnPollData.getOptionIndex());
|
||||||
|
|
||||||
|
try {
|
||||||
|
saveHelper.execute(this.repository);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to save poll vote into repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(String pollName, byte[] voterPublicKey) throws DataException {
|
||||||
|
try {
|
||||||
|
this.repository.checkedExecute("DELETE FROM PollVotes WHERE poll_name = ? AND voter = ?", pollName, voterPublicKey);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to delete poll vote from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
|||||||
private HSQLDBGenesisTransactionRepository genesisTransactionRepository;
|
private HSQLDBGenesisTransactionRepository genesisTransactionRepository;
|
||||||
private HSQLDBPaymentTransactionRepository paymentTransactionRepository;
|
private HSQLDBPaymentTransactionRepository paymentTransactionRepository;
|
||||||
private HSQLDBCreatePollTransactionRepository createPollTransactionRepository;
|
private HSQLDBCreatePollTransactionRepository createPollTransactionRepository;
|
||||||
|
private HSQLDBVoteOnPollTransactionRepository voteOnPollTransactionRepository;
|
||||||
private HSQLDBIssueAssetTransactionRepository issueAssetTransactionRepository;
|
private HSQLDBIssueAssetTransactionRepository issueAssetTransactionRepository;
|
||||||
private HSQLDBTransferAssetTransactionRepository transferAssetTransactionRepository;
|
private HSQLDBTransferAssetTransactionRepository transferAssetTransactionRepository;
|
||||||
private HSQLDBCreateOrderTransactionRepository createOrderTransactionRepository;
|
private HSQLDBCreateOrderTransactionRepository createOrderTransactionRepository;
|
||||||
@ -34,6 +35,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
|||||||
this.genesisTransactionRepository = new HSQLDBGenesisTransactionRepository(repository);
|
this.genesisTransactionRepository = new HSQLDBGenesisTransactionRepository(repository);
|
||||||
this.paymentTransactionRepository = new HSQLDBPaymentTransactionRepository(repository);
|
this.paymentTransactionRepository = new HSQLDBPaymentTransactionRepository(repository);
|
||||||
this.createPollTransactionRepository = new HSQLDBCreatePollTransactionRepository(repository);
|
this.createPollTransactionRepository = new HSQLDBCreatePollTransactionRepository(repository);
|
||||||
|
this.voteOnPollTransactionRepository = new HSQLDBVoteOnPollTransactionRepository(repository);
|
||||||
this.issueAssetTransactionRepository = new HSQLDBIssueAssetTransactionRepository(repository);
|
this.issueAssetTransactionRepository = new HSQLDBIssueAssetTransactionRepository(repository);
|
||||||
this.transferAssetTransactionRepository = new HSQLDBTransferAssetTransactionRepository(repository);
|
this.transferAssetTransactionRepository = new HSQLDBTransferAssetTransactionRepository(repository);
|
||||||
this.createOrderTransactionRepository = new HSQLDBCreateOrderTransactionRepository(repository);
|
this.createOrderTransactionRepository = new HSQLDBCreateOrderTransactionRepository(repository);
|
||||||
@ -93,6 +95,9 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
|||||||
case CREATE_POLL:
|
case CREATE_POLL:
|
||||||
return this.createPollTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
|
return this.createPollTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
|
||||||
|
|
||||||
|
case VOTE_ON_POLL:
|
||||||
|
return this.voteOnPollTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
|
||||||
|
|
||||||
case ISSUE_ASSET:
|
case ISSUE_ASSET:
|
||||||
return this.issueAssetTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
|
return this.issueAssetTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
|
||||||
|
|
||||||
@ -219,6 +224,10 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
|||||||
this.createPollTransactionRepository.save(transactionData);
|
this.createPollTransactionRepository.save(transactionData);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case VOTE_ON_POLL:
|
||||||
|
this.voteOnPollTransactionRepository.save(transactionData);
|
||||||
|
break;
|
||||||
|
|
||||||
case ISSUE_ASSET:
|
case ISSUE_ASSET:
|
||||||
this.issueAssetTransactionRepository.save(transactionData);
|
this.issueAssetTransactionRepository.save(transactionData);
|
||||||
break;
|
break;
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
package repository.hsqldb.transaction;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
import data.transaction.VoteOnPollTransactionData;
|
||||||
|
import data.transaction.TransactionData;
|
||||||
|
import repository.DataException;
|
||||||
|
import repository.hsqldb.HSQLDBRepository;
|
||||||
|
import repository.hsqldb.HSQLDBSaver;
|
||||||
|
|
||||||
|
public class HSQLDBVoteOnPollTransactionRepository extends HSQLDBTransactionRepository {
|
||||||
|
|
||||||
|
public HSQLDBVoteOnPollTransactionRepository(HSQLDBRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
|
||||||
|
try {
|
||||||
|
ResultSet rs = this.repository
|
||||||
|
.checkedExecute("SELECT poll_name, option_index, previous_option_index FROM VoteOnPollTransactions WHERE signature = ?", signature);
|
||||||
|
if (rs == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
String pollName = rs.getString(1);
|
||||||
|
int optionIndex = rs.getInt(2);
|
||||||
|
Integer previousOptionIndex = rs.getInt(3);
|
||||||
|
|
||||||
|
return new VoteOnPollTransactionData(creatorPublicKey, pollName, optionIndex, previousOptionIndex, fee, timestamp, reference, signature);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch vote on poll transaction from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(TransactionData transactionData) throws DataException {
|
||||||
|
VoteOnPollTransactionData voteOnPollTransactionData = (VoteOnPollTransactionData) transactionData;
|
||||||
|
|
||||||
|
HSQLDBSaver saveHelper = new HSQLDBSaver("VoteOnPollTransactions");
|
||||||
|
|
||||||
|
saveHelper.bind("signature", voteOnPollTransactionData.getSignature()).bind("poll_name", voteOnPollTransactionData.getPollName())
|
||||||
|
.bind("voter", voteOnPollTransactionData.getVoterPublicKey()).bind("option_index", voteOnPollTransactionData.getOptionIndex())
|
||||||
|
.bind("previous_option_index", voteOnPollTransactionData.getPreviousOptionIndex());
|
||||||
|
|
||||||
|
try {
|
||||||
|
saveHelper.execute(this.repository);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to save vote on poll transaction into repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,13 +1,38 @@
|
|||||||
package settings;
|
package settings;
|
||||||
|
|
||||||
|
import qora.block.GenesisBlock;
|
||||||
|
|
||||||
public class Settings {
|
public class Settings {
|
||||||
|
|
||||||
|
private static Settings instance;
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
private long genesisTimestamp = -1;
|
||||||
|
|
||||||
public static Settings getInstance() {
|
public static Settings getInstance() {
|
||||||
return new Settings();
|
if (instance == null)
|
||||||
|
instance = new Settings();
|
||||||
|
|
||||||
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getMaxBytePerFee() {
|
public int getMaxBytePerFee() {
|
||||||
return 1024;
|
return 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getGenesisTimestamp() {
|
||||||
|
if (this.genesisTimestamp != -1)
|
||||||
|
return this.genesisTimestamp;
|
||||||
|
|
||||||
|
return GenesisBlock.GENESIS_TIMESTAMP;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGenesisTimestamp(long timestamp) {
|
||||||
|
this.genesisTimestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unsetGenesisTimestamp() {
|
||||||
|
this.genesisTimestamp = -1;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,10 @@ import static org.junit.Assert.*;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.AfterClass;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.BeforeClass;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import com.google.common.hash.HashCode;
|
import com.google.common.hash.HashCode;
|
||||||
@ -19,6 +17,7 @@ import data.account.AccountData;
|
|||||||
import data.block.BlockData;
|
import data.block.BlockData;
|
||||||
import data.transaction.CreatePollTransactionData;
|
import data.transaction.CreatePollTransactionData;
|
||||||
import data.transaction.PaymentTransactionData;
|
import data.transaction.PaymentTransactionData;
|
||||||
|
import data.transaction.VoteOnPollTransactionData;
|
||||||
import data.voting.PollData;
|
import data.voting.PollData;
|
||||||
import data.voting.PollOptionData;
|
import data.voting.PollOptionData;
|
||||||
import qora.account.Account;
|
import qora.account.Account;
|
||||||
@ -31,12 +30,14 @@ import qora.transaction.CreatePollTransaction;
|
|||||||
import qora.transaction.PaymentTransaction;
|
import qora.transaction.PaymentTransaction;
|
||||||
import qora.transaction.Transaction;
|
import qora.transaction.Transaction;
|
||||||
import qora.transaction.Transaction.ValidationResult;
|
import qora.transaction.Transaction.ValidationResult;
|
||||||
|
import qora.transaction.VoteOnPollTransaction;
|
||||||
import repository.AccountRepository;
|
import repository.AccountRepository;
|
||||||
import repository.DataException;
|
import repository.DataException;
|
||||||
import repository.Repository;
|
import repository.Repository;
|
||||||
import repository.RepositoryFactory;
|
import repository.RepositoryFactory;
|
||||||
import repository.RepositoryManager;
|
import repository.RepositoryManager;
|
||||||
import repository.hsqldb.HSQLDBRepositoryFactory;
|
import repository.hsqldb.HSQLDBRepositoryFactory;
|
||||||
|
import settings.Settings;
|
||||||
|
|
||||||
// Don't extend Common as we want to use an in-memory database
|
// Don't extend Common as we want to use an in-memory database
|
||||||
public class TransactionTests {
|
public class TransactionTests {
|
||||||
@ -57,8 +58,7 @@ public class TransactionTests {
|
|||||||
private PrivateKeyAccount generator;
|
private PrivateKeyAccount generator;
|
||||||
private byte[] reference;
|
private byte[] reference;
|
||||||
|
|
||||||
@Before
|
public void createTestAccounts(Long genesisTimestamp) throws DataException {
|
||||||
public void createTestAccounts() throws DataException {
|
|
||||||
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
|
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
|
||||||
RepositoryManager.setRepositoryFactory(repositoryFactory);
|
RepositoryManager.setRepositoryFactory(repositoryFactory);
|
||||||
|
|
||||||
@ -66,6 +66,12 @@ public class TransactionTests {
|
|||||||
assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight());
|
assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Un]set genesis timestamp as required by test
|
||||||
|
if (genesisTimestamp != null)
|
||||||
|
Settings.getInstance().setGenesisTimestamp(genesisTimestamp);
|
||||||
|
else
|
||||||
|
Settings.getInstance().unsetGenesisTimestamp();
|
||||||
|
|
||||||
// This needs to be called outside of acquiring our own repository or it will deadlock
|
// This needs to be called outside of acquiring our own repository or it will deadlock
|
||||||
BlockChain.validate();
|
BlockChain.validate();
|
||||||
|
|
||||||
@ -102,6 +108,8 @@ public class TransactionTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPaymentTransaction() throws DataException {
|
public void testPaymentTransaction() throws DataException {
|
||||||
|
createTestAccounts(null);
|
||||||
|
|
||||||
// Make a new payment transaction
|
// Make a new payment transaction
|
||||||
Account recipient = new PublicKeyAccount(repository, recipientSeed);
|
Account recipient = new PublicKeyAccount(repository, recipientSeed);
|
||||||
BigDecimal amount = BigDecimal.valueOf(1_000L);
|
BigDecimal amount = BigDecimal.valueOf(1_000L);
|
||||||
@ -144,7 +152,8 @@ public class TransactionTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreatePollTransaction() throws DataException {
|
public void testCreatePollTransaction() throws DataException {
|
||||||
// XXX This test fails unless GenesisBlock's timestamp is set to something after BlockChain.VOTING_RELEASE_TIMESTAMP so we need better testing setup
|
// This test requires GenesisBlock's timestamp is set to something after BlockChain.VOTING_RELEASE_TIMESTAMP
|
||||||
|
createTestAccounts(BlockChain.VOTING_RELEASE_TIMESTAMP + 1_000L);
|
||||||
|
|
||||||
// Make a new create poll transaction
|
// Make a new create poll transaction
|
||||||
String pollName = "test poll";
|
String pollName = "test poll";
|
||||||
@ -166,7 +175,7 @@ public class TransactionTests {
|
|||||||
assertTrue(createPollTransaction.isSignatureValid());
|
assertTrue(createPollTransaction.isSignatureValid());
|
||||||
assertEquals(ValidationResult.OK, createPollTransaction.isValid());
|
assertEquals(ValidationResult.OK, createPollTransaction.isValid());
|
||||||
|
|
||||||
// Forge new block with payment transaction
|
// Forge new block with transaction
|
||||||
Block block = new Block(repository, genesisBlockData, generator, null, null);
|
Block block = new Block(repository, genesisBlockData, generator, null, null);
|
||||||
block.addTransaction(createPollTransactionData);
|
block.addTransaction(createPollTransactionData);
|
||||||
block.sign();
|
block.sign();
|
||||||
@ -190,6 +199,57 @@ public class TransactionTests {
|
|||||||
// Check poll was created
|
// Check poll was created
|
||||||
PollData actualPollData = this.repository.getVotingRepository().fromPollName(pollName);
|
PollData actualPollData = this.repository.getVotingRepository().fromPollName(pollName);
|
||||||
assertNotNull(actualPollData);
|
assertNotNull(actualPollData);
|
||||||
|
|
||||||
|
// Check sender's reference
|
||||||
|
assertTrue("Sender's new reference incorrect", Arrays.equals(createPollTransactionData.getSignature(), sender.getLastReference()));
|
||||||
|
|
||||||
|
// Update reference variable for use by other tests
|
||||||
|
reference = sender.getLastReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVoteOnPollTransaction() throws DataException {
|
||||||
|
// Create poll using another test
|
||||||
|
testCreatePollTransaction();
|
||||||
|
|
||||||
|
// Try all options, plus invalid optionIndex (note use of <= for this)
|
||||||
|
String pollName = "test poll";
|
||||||
|
int pollOptionsSize = 3;
|
||||||
|
BigDecimal fee = BigDecimal.ONE;
|
||||||
|
long timestamp = genesisBlockData.getTimestamp() + 1_000;
|
||||||
|
BlockData previousBlockData = genesisBlockData;
|
||||||
|
|
||||||
|
for (int optionIndex = 0; optionIndex <= pollOptionsSize; ++optionIndex) {
|
||||||
|
// Make a vote-on-poll transaction
|
||||||
|
VoteOnPollTransactionData voteOnPollTransactionData = new VoteOnPollTransactionData(sender.getPublicKey(), pollName, optionIndex, fee, timestamp,
|
||||||
|
reference);
|
||||||
|
|
||||||
|
Transaction voteOnPollTransaction = new VoteOnPollTransaction(repository, voteOnPollTransactionData);
|
||||||
|
voteOnPollTransaction.calcSignature(sender);
|
||||||
|
assertTrue(voteOnPollTransaction.isSignatureValid());
|
||||||
|
|
||||||
|
if (optionIndex == pollOptionsSize) {
|
||||||
|
assertEquals(ValidationResult.POLL_OPTION_DOES_NOT_EXIST, voteOnPollTransaction.isValid());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
assertEquals(ValidationResult.OK, voteOnPollTransaction.isValid());
|
||||||
|
|
||||||
|
// Forge new block with transaction
|
||||||
|
Block block = new Block(repository, previousBlockData, generator, null, null);
|
||||||
|
block.addTransaction(voteOnPollTransactionData);
|
||||||
|
block.sign();
|
||||||
|
|
||||||
|
assertTrue("Block signatures invalid", block.isSignatureValid());
|
||||||
|
assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
|
||||||
|
|
||||||
|
block.process();
|
||||||
|
repository.saveChanges();
|
||||||
|
|
||||||
|
// update variables for next round
|
||||||
|
previousBlockData = block.getBlockData();
|
||||||
|
timestamp += 1_000;
|
||||||
|
reference = voteOnPollTransaction.getTransactionData().getSignature();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -18,7 +18,7 @@ import data.transaction.CreatePollTransactionData;
|
|||||||
import data.transaction.TransactionData;
|
import data.transaction.TransactionData;
|
||||||
import data.voting.PollOptionData;
|
import data.voting.PollOptionData;
|
||||||
import qora.account.PublicKeyAccount;
|
import qora.account.PublicKeyAccount;
|
||||||
import qora.transaction.CreatePollTransaction;
|
import qora.voting.Poll;
|
||||||
import transform.TransformationException;
|
import transform.TransformationException;
|
||||||
import utils.Base58;
|
import utils.Base58;
|
||||||
import utils.Serialization;
|
import utils.Serialization;
|
||||||
@ -46,20 +46,20 @@ public class CreatePollTransactionTransformer extends TransactionTransformer {
|
|||||||
|
|
||||||
String owner = Serialization.deserializeRecipient(byteBuffer);
|
String owner = Serialization.deserializeRecipient(byteBuffer);
|
||||||
|
|
||||||
String pollName = Serialization.deserializeSizedString(byteBuffer, CreatePollTransaction.MAX_NAME_SIZE);
|
String pollName = Serialization.deserializeSizedString(byteBuffer, Poll.MAX_NAME_SIZE);
|
||||||
String description = Serialization.deserializeSizedString(byteBuffer, CreatePollTransaction.MAX_DESCRIPTION_SIZE);
|
String description = Serialization.deserializeSizedString(byteBuffer, Poll.MAX_DESCRIPTION_SIZE);
|
||||||
|
|
||||||
// Make sure there are enough bytes left for poll options
|
// Make sure there are enough bytes left for poll options
|
||||||
if (byteBuffer.remaining() < OPTIONS_SIZE_LENGTH)
|
if (byteBuffer.remaining() < OPTIONS_SIZE_LENGTH)
|
||||||
throw new TransformationException("Byte data too short for CreatePollTransaction");
|
throw new TransformationException("Byte data too short for CreatePollTransaction");
|
||||||
|
|
||||||
int optionsCount = byteBuffer.getInt();
|
int optionsCount = byteBuffer.getInt();
|
||||||
if (optionsCount < 1 || optionsCount > CreatePollTransaction.MAX_OPTIONS)
|
if (optionsCount < 1 || optionsCount > Poll.MAX_OPTIONS)
|
||||||
throw new TransformationException("Invalid number of options for CreatePollTransaction");
|
throw new TransformationException("Invalid number of options for CreatePollTransaction");
|
||||||
|
|
||||||
List<PollOptionData> pollOptions = new ArrayList<PollOptionData>();
|
List<PollOptionData> pollOptions = new ArrayList<PollOptionData>();
|
||||||
for (int i = 0; i < optionsCount; ++i) {
|
for (int optionIndex = 0; optionIndex < optionsCount; ++optionIndex) {
|
||||||
String optionName = Serialization.deserializeSizedString(byteBuffer, CreatePollTransaction.MAX_NAME_SIZE);
|
String optionName = Serialization.deserializeSizedString(byteBuffer, Poll.MAX_NAME_SIZE);
|
||||||
|
|
||||||
pollOptions.add(new PollOptionData(optionName));
|
pollOptions.add(new PollOptionData(optionName));
|
||||||
}
|
}
|
||||||
@ -106,9 +106,8 @@ public class CreatePollTransactionTransformer extends TransactionTransformer {
|
|||||||
List<PollOptionData> pollOptions = createPollTransactionData.getPollOptions();
|
List<PollOptionData> pollOptions = createPollTransactionData.getPollOptions();
|
||||||
bytes.write(Ints.toByteArray(pollOptions.size()));
|
bytes.write(Ints.toByteArray(pollOptions.size()));
|
||||||
|
|
||||||
for (PollOptionData pollOptionData : pollOptions) {
|
for (PollOptionData pollOptionData : pollOptions)
|
||||||
Serialization.serializeSizedString(bytes, pollOptionData.getOptionName());
|
Serialization.serializeSizedString(bytes, pollOptionData.getOptionName());
|
||||||
}
|
|
||||||
|
|
||||||
Serialization.serializeBigDecimal(bytes, createPollTransactionData.getFee());
|
Serialization.serializeBigDecimal(bytes, createPollTransactionData.getFee());
|
||||||
|
|
||||||
@ -138,9 +137,8 @@ public class CreatePollTransactionTransformer extends TransactionTransformer {
|
|||||||
json.put("description", createPollTransactionData.getDescription());
|
json.put("description", createPollTransactionData.getDescription());
|
||||||
|
|
||||||
JSONArray options = new JSONArray();
|
JSONArray options = new JSONArray();
|
||||||
for (PollOptionData optionData : createPollTransactionData.getPollOptions()) {
|
for (PollOptionData optionData : createPollTransactionData.getPollOptions())
|
||||||
options.add(optionData.getOptionName());
|
options.add(optionData.getOptionName());
|
||||||
}
|
|
||||||
|
|
||||||
json.put("options", options);
|
json.put("options", options);
|
||||||
} catch (ClassCastException e) {
|
} catch (ClassCastException e) {
|
||||||
|
@ -40,6 +40,9 @@ public class TransactionTransformer extends Transformer {
|
|||||||
case CREATE_POLL:
|
case CREATE_POLL:
|
||||||
return CreatePollTransactionTransformer.fromByteBuffer(byteBuffer);
|
return CreatePollTransactionTransformer.fromByteBuffer(byteBuffer);
|
||||||
|
|
||||||
|
case VOTE_ON_POLL:
|
||||||
|
return VoteOnPollTransactionTransformer.fromByteBuffer(byteBuffer);
|
||||||
|
|
||||||
case ISSUE_ASSET:
|
case ISSUE_ASSET:
|
||||||
return IssueAssetTransactionTransformer.fromByteBuffer(byteBuffer);
|
return IssueAssetTransactionTransformer.fromByteBuffer(byteBuffer);
|
||||||
|
|
||||||
@ -74,6 +77,9 @@ public class TransactionTransformer extends Transformer {
|
|||||||
case CREATE_POLL:
|
case CREATE_POLL:
|
||||||
return CreatePollTransactionTransformer.getDataLength(transactionData);
|
return CreatePollTransactionTransformer.getDataLength(transactionData);
|
||||||
|
|
||||||
|
case VOTE_ON_POLL:
|
||||||
|
return VoteOnPollTransactionTransformer.getDataLength(transactionData);
|
||||||
|
|
||||||
case ISSUE_ASSET:
|
case ISSUE_ASSET:
|
||||||
return IssueAssetTransactionTransformer.getDataLength(transactionData);
|
return IssueAssetTransactionTransformer.getDataLength(transactionData);
|
||||||
|
|
||||||
@ -108,6 +114,9 @@ public class TransactionTransformer extends Transformer {
|
|||||||
case CREATE_POLL:
|
case CREATE_POLL:
|
||||||
return CreatePollTransactionTransformer.toBytes(transactionData);
|
return CreatePollTransactionTransformer.toBytes(transactionData);
|
||||||
|
|
||||||
|
case VOTE_ON_POLL:
|
||||||
|
return VoteOnPollTransactionTransformer.toBytes(transactionData);
|
||||||
|
|
||||||
case ISSUE_ASSET:
|
case ISSUE_ASSET:
|
||||||
return IssueAssetTransactionTransformer.toBytes(transactionData);
|
return IssueAssetTransactionTransformer.toBytes(transactionData);
|
||||||
|
|
||||||
@ -142,6 +151,9 @@ public class TransactionTransformer extends Transformer {
|
|||||||
case CREATE_POLL:
|
case CREATE_POLL:
|
||||||
return CreatePollTransactionTransformer.toJSON(transactionData);
|
return CreatePollTransactionTransformer.toJSON(transactionData);
|
||||||
|
|
||||||
|
case VOTE_ON_POLL:
|
||||||
|
return VoteOnPollTransactionTransformer.toJSON(transactionData);
|
||||||
|
|
||||||
case ISSUE_ASSET:
|
case ISSUE_ASSET:
|
||||||
return IssueAssetTransactionTransformer.toJSON(transactionData);
|
return IssueAssetTransactionTransformer.toJSON(transactionData);
|
||||||
|
|
||||||
|
117
src/transform/transaction/VoteOnPollTransactionTransformer.java
Normal file
117
src/transform/transaction/VoteOnPollTransactionTransformer.java
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package transform.transaction;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import org.json.simple.JSONObject;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
|
import com.google.common.primitives.Ints;
|
||||||
|
import com.google.common.primitives.Longs;
|
||||||
|
|
||||||
|
import data.transaction.TransactionData;
|
||||||
|
import data.transaction.VoteOnPollTransactionData;
|
||||||
|
import qora.account.PublicKeyAccount;
|
||||||
|
import qora.voting.Poll;
|
||||||
|
import transform.TransformationException;
|
||||||
|
import utils.Serialization;
|
||||||
|
|
||||||
|
public class VoteOnPollTransactionTransformer extends TransactionTransformer {
|
||||||
|
|
||||||
|
// Property lengths
|
||||||
|
private static final int VOTER_LENGTH = ADDRESS_LENGTH;
|
||||||
|
private static final int NAME_SIZE_LENGTH = INT_LENGTH;
|
||||||
|
private static final int OPTION_LENGTH = INT_LENGTH;
|
||||||
|
|
||||||
|
private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + VOTER_LENGTH + NAME_SIZE_LENGTH;
|
||||||
|
|
||||||
|
static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
|
||||||
|
if (byteBuffer.remaining() < TYPELESS_DATALESS_LENGTH)
|
||||||
|
throw new TransformationException("Byte data too short for VoteOnPollTransaction");
|
||||||
|
|
||||||
|
long timestamp = byteBuffer.getLong();
|
||||||
|
|
||||||
|
byte[] reference = new byte[REFERENCE_LENGTH];
|
||||||
|
byteBuffer.get(reference);
|
||||||
|
|
||||||
|
byte[] voterPublicKey = Serialization.deserializePublicKey(byteBuffer);
|
||||||
|
|
||||||
|
String pollName = Serialization.deserializeSizedString(byteBuffer, Poll.MAX_NAME_SIZE);
|
||||||
|
|
||||||
|
// Make sure there are enough bytes left for poll options
|
||||||
|
if (byteBuffer.remaining() < OPTION_LENGTH)
|
||||||
|
throw new TransformationException("Byte data too short for VoteOnPollTransaction");
|
||||||
|
|
||||||
|
int optionIndex = byteBuffer.getInt();
|
||||||
|
if (optionIndex < 0 || optionIndex >= Poll.MAX_OPTIONS)
|
||||||
|
throw new TransformationException("Invalid option number for VoteOnPollTransaction");
|
||||||
|
|
||||||
|
// Still need to make sure there are enough bytes left for remaining fields
|
||||||
|
if (byteBuffer.remaining() < FEE_LENGTH + SIGNATURE_LENGTH)
|
||||||
|
throw new TransformationException("Byte data too short for VoteOnPollTransaction");
|
||||||
|
|
||||||
|
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
|
||||||
|
|
||||||
|
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||||
|
byteBuffer.get(signature);
|
||||||
|
|
||||||
|
return new VoteOnPollTransactionData(voterPublicKey, pollName, optionIndex, fee, timestamp, reference, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getDataLength(TransactionData transactionData) throws TransformationException {
|
||||||
|
VoteOnPollTransactionData voteOnPollTransactionData = (VoteOnPollTransactionData) transactionData;
|
||||||
|
|
||||||
|
int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + voteOnPollTransactionData.getPollName().length();
|
||||||
|
|
||||||
|
return dataLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
|
||||||
|
try {
|
||||||
|
VoteOnPollTransactionData voteOnPollTransactionData = (VoteOnPollTransactionData) transactionData;
|
||||||
|
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
bytes.write(Ints.toByteArray(voteOnPollTransactionData.getType().value));
|
||||||
|
bytes.write(Longs.toByteArray(voteOnPollTransactionData.getTimestamp()));
|
||||||
|
bytes.write(voteOnPollTransactionData.getReference());
|
||||||
|
|
||||||
|
bytes.write(voteOnPollTransactionData.getVoterPublicKey());
|
||||||
|
Serialization.serializeSizedString(bytes, voteOnPollTransactionData.getPollName());
|
||||||
|
bytes.write(voteOnPollTransactionData.getOptionIndex());
|
||||||
|
|
||||||
|
Serialization.serializeBigDecimal(bytes, voteOnPollTransactionData.getFee());
|
||||||
|
|
||||||
|
if (voteOnPollTransactionData.getSignature() != null)
|
||||||
|
bytes.write(voteOnPollTransactionData.getSignature());
|
||||||
|
|
||||||
|
return bytes.toByteArray();
|
||||||
|
} catch (IOException | ClassCastException e) {
|
||||||
|
throw new TransformationException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static JSONObject toJSON(TransactionData transactionData) throws TransformationException {
|
||||||
|
JSONObject json = TransactionTransformer.getBaseJSON(transactionData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
VoteOnPollTransactionData voteOnPollTransactionData = (VoteOnPollTransactionData) transactionData;
|
||||||
|
|
||||||
|
byte[] voterPublicKey = voteOnPollTransactionData.getVoterPublicKey();
|
||||||
|
|
||||||
|
json.put("voter", PublicKeyAccount.getAddress(voterPublicKey));
|
||||||
|
json.put("voterPublicKey", HashCode.fromBytes(voterPublicKey).toString());
|
||||||
|
|
||||||
|
json.put("name", voteOnPollTransactionData.getPollName());
|
||||||
|
json.put("optionIndex", voteOnPollTransactionData.getOptionIndex());
|
||||||
|
} catch (ClassCastException e) {
|
||||||
|
throw new TransformationException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user