forked from Qortal/qortal
Browse Source
* 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 testsplit-DB
catbref
6 years ago
18 changed files with 630 additions and 41 deletions
@ -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; |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
|
||||
} |
@ -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; |
||||
|
||||
import qora.block.GenesisBlock; |
||||
|
||||
public class Settings { |
||||
|
||||
private static Settings instance; |
||||
|
||||
// Properties
|
||||
private long genesisTimestamp = -1; |
||||
|
||||
public static Settings getInstance() { |
||||
return new Settings(); |
||||
if (instance == null) |
||||
instance = new Settings(); |
||||
|
||||
return instance; |
||||
} |
||||
|
||||
public int getMaxBytePerFee() { |
||||
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; |
||||
} |
||||
|
||||
} |
||||
|
@ -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…
Reference in new issue