From 5e674cbaab8ddf4dacade1670de0a988b64ace6a Mon Sep 17 00:00:00 2001 From: catbref Date: Tue, 19 Jun 2018 12:30:17 +0100 Subject: [PATCH] Added Create Poll Transaction support (untested) * Moved Asset issue/deissue code from IssueAssetTransaction to Asset business object. * Added more constructors for Asset using IssueAssetTransactionData or assetId. * Moved some constants from transaction transfers to business objects (e.g. IssueAssetTransaction) (They might now make more sense being in Asset) * Changed some transaction isValid() checks to use transaction's timestamp instead of NTP.getTime() * New VotingRepository - as yet unimplemented Really need to rewrite "migrate" and add a ton of unit tests. --- .../CreatePollTransactionData.java | 53 +++++ src/data/voting/PollData.java | 47 +++++ src/data/voting/PollOptionData.java | 20 ++ src/qora/assets/Asset.java | 30 +++ src/qora/block/BlockChain.java | 2 +- .../transaction/CreatePollTransaction.java | 183 ++++++++++++++++++ .../transaction/IssueAssetTransaction.java | 28 +-- src/qora/transaction/MessageTransaction.java | 4 +- src/qora/transaction/Transaction.java | 10 +- src/qora/voting/Poll.java | 53 +++++ src/repository/Repository.java | 2 + src/repository/VotingRepository.java | 15 ++ .../hsqldb/HSQLDBDatabaseUpdates.java | 6 +- src/repository/hsqldb/HSQLDBRepository.java | 6 + .../hsqldb/HSQLDBVotingRepository.java | 33 ++++ ...HSQLDBCreatePollTransactionRepository.java | 81 ++++++++ .../HSQLDBTransactionRepository.java | 11 ++ .../CreatePollTransactionTransformer.java | 151 +++++++++++++++ .../IssueAssetTransactionTransformer.java | 11 +- .../MessageTransactionTransformer.java | 9 +- .../transaction/TransactionTransformer.java | 12 ++ src/utils/Serialization.java | 8 +- 22 files changed, 740 insertions(+), 35 deletions(-) create mode 100644 src/data/transaction/CreatePollTransactionData.java create mode 100644 src/data/voting/PollData.java create mode 100644 src/data/voting/PollOptionData.java create mode 100644 src/qora/transaction/CreatePollTransaction.java create mode 100644 src/qora/voting/Poll.java create mode 100644 src/repository/VotingRepository.java create mode 100644 src/repository/hsqldb/HSQLDBVotingRepository.java create mode 100644 src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java create mode 100644 src/transform/transaction/CreatePollTransactionTransformer.java diff --git a/src/data/transaction/CreatePollTransactionData.java b/src/data/transaction/CreatePollTransactionData.java new file mode 100644 index 00000000..df00c896 --- /dev/null +++ b/src/data/transaction/CreatePollTransactionData.java @@ -0,0 +1,53 @@ +package data.transaction; + +import java.math.BigDecimal; +import java.util.List; + +import data.voting.PollOptionData; +import qora.transaction.Transaction; + +public class CreatePollTransactionData extends TransactionData { + + // Properties + private byte[] creatorPublicKey; + private String owner; + private String pollName; + private String description; + private List pollOptions; + + // Constructors + + public CreatePollTransactionData(byte[] creatorPublicKey, String owner, String pollName, String description, List pollOptions, + BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + super(Transaction.TransactionType.CREATE_POLL, fee, creatorPublicKey, timestamp, reference, signature); + + this.creatorPublicKey = creatorPublicKey; + this.owner = owner; + this.pollName = pollName; + this.description = description; + this.pollOptions = pollOptions; + } + + // Getters/setters + + public byte[] getCreatorPublicKey() { + return this.creatorPublicKey; + } + + public String getOwner() { + return this.owner; + } + + public String getPollName() { + return this.pollName; + } + + public String getDescription() { + return this.description; + } + + public List getPollOptions() { + return this.pollOptions; + } + +} diff --git a/src/data/voting/PollData.java b/src/data/voting/PollData.java new file mode 100644 index 00000000..eab99ee1 --- /dev/null +++ b/src/data/voting/PollData.java @@ -0,0 +1,47 @@ +package data.voting; + +import java.util.List; + +import data.voting.PollOptionData; + +public class PollData { + + // Properties + private byte[] creatorPublicKey; + private String owner; + private String pollName; + private String description; + private List pollOptions; + + // Constructors + + public PollData(byte[] creatorPublicKey, String owner, String pollName, String description, List pollOptions) { + this.creatorPublicKey = creatorPublicKey; + this.pollName = pollName; + this.description = description; + this.pollOptions = pollOptions; + } + + // Getters/setters + + public byte[] getCreatorPublicKey() { + return this.creatorPublicKey; + } + + public String getOwner() { + return this.owner; + } + + public String getPollName() { + return this.pollName; + } + + public String getDescription() { + return this.description; + } + + public List getPollOptions() { + return this.pollOptions; + } + +} diff --git a/src/data/voting/PollOptionData.java b/src/data/voting/PollOptionData.java new file mode 100644 index 00000000..01fd26a6 --- /dev/null +++ b/src/data/voting/PollOptionData.java @@ -0,0 +1,20 @@ +package data.voting; + +public class PollOptionData { + + // Properties + private String optionName; + + // Constructors + + public PollOptionData(String optionName) { + this.optionName = optionName; + } + + // Getters/setters + + public String getOptionName() { + return this.optionName; + } + +} diff --git a/src/qora/assets/Asset.java b/src/qora/assets/Asset.java index e105545a..c19f6d07 100644 --- a/src/qora/assets/Asset.java +++ b/src/qora/assets/Asset.java @@ -1,6 +1,8 @@ package qora.assets; import data.assets.AssetData; +import data.transaction.IssueAssetTransactionData; +import repository.DataException; import repository.Repository; public class Asset { @@ -21,4 +23,32 @@ public class Asset { this.assetData = assetData; } + public Asset(Repository repository, IssueAssetTransactionData issueAssetTransactionData) { + this.repository = repository; + this.assetData = new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(), + issueAssetTransactionData.getDescription(), issueAssetTransactionData.getQuantity(), issueAssetTransactionData.getIsDivisible(), + issueAssetTransactionData.getReference()); + } + + public Asset(Repository repository, long assetId) throws DataException { + this.repository = repository; + this.assetData = this.repository.getAssetRepository().fromAssetId(assetId); + } + + // Getters/setters + + public AssetData getAssetData() { + return this.assetData; + } + + // Processing + + public void issue() throws DataException { + this.repository.getAssetRepository().save(this.assetData); + } + + public void deissue() throws DataException { + this.repository.getAssetRepository().delete(this.assetData.getAssetId()); + } + } diff --git a/src/qora/block/BlockChain.java b/src/qora/block/BlockChain.java index 2adfd3a8..053fe2b9 100644 --- a/src/qora/block/BlockChain.java +++ b/src/qora/block/BlockChain.java @@ -47,7 +47,7 @@ public class BlockChain { public static final int AT_BLOCK_HEIGHT_RELEASE = 99000; public static final long POWFIX_RELEASE_TIMESTAMP = 1456426800000L; // Block Version 3 // 2016-02-25T19:00:00+00:00 public static final long ASSETS_RELEASE_TIMESTAMP = 0L; // From Qora epoch - + public static final long VOTING_RELEASE_TIMESTAMP = 1403715600000L; // 2014-06-25T17:00:00+00:00 /** * Some sort start-up/initialization/checking method. diff --git a/src/qora/transaction/CreatePollTransaction.java b/src/qora/transaction/CreatePollTransaction.java new file mode 100644 index 00000000..aeb87639 --- /dev/null +++ b/src/qora/transaction/CreatePollTransaction.java @@ -0,0 +1,183 @@ +package qora.transaction; + +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import data.transaction.CreatePollTransactionData; +import data.transaction.TransactionData; +import data.voting.PollOptionData; +import qora.account.Account; +import qora.account.PublicKeyAccount; +import qora.assets.Asset; +import qora.block.BlockChain; +import qora.crypto.Crypto; +import qora.voting.Poll; +import repository.DataException; +import repository.Repository; + +public class CreatePollTransaction extends Transaction { + + // Properties + 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 + + public CreatePollTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + + this.createPollTransactionData = (CreatePollTransactionData) this.transactionData; + } + + // More information + + public List getRecipientAccounts() throws DataException { + return Collections.singletonList(getOwner()); + } + + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getCreator().getAddress())) + return true; + + if (address.equals(this.getOwner().getAddress())) + return true; + + return false; + } + + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + + if (address.equals(this.getCreator().getAddress())) + amount = amount.subtract(this.transactionData.getFee()); + + return amount; + } + + // Navigation + + public Account getCreator() throws DataException { + return new PublicKeyAccount(this.repository, this.createPollTransactionData.getCreatorPublicKey()); + } + + public Account getOwner() throws DataException { + return new Account(this.repository, this.createPollTransactionData.getOwner()); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + // Are CreatePollTransactions even allowed at this point? + // XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used? + if (this.createPollTransactionData.getTimestamp() < BlockChain.VOTING_RELEASE_TIMESTAMP) + return ValidationResult.NOT_YET_RELEASED; + + // Check owner address is valid + if (!Crypto.isValidAddress(createPollTransactionData.getOwner())) + return ValidationResult.INVALID_ADDRESS; + + // Check name size bounds + if (createPollTransactionData.getPollName().length() < 1 || createPollTransactionData.getPollName().length() > CreatePollTransaction.MAX_NAME_SIZE) + return ValidationResult.INVALID_NAME_LENGTH; + + // Check description size bounds + if (createPollTransactionData.getDescription().length() < 1 + || createPollTransactionData.getDescription().length() > CreatePollTransaction.MAX_DESCRIPTION_SIZE) + return ValidationResult.INVALID_DESCRIPTION_LENGTH; + + // Check poll name is lowercase + if (!createPollTransactionData.getPollName().equals(createPollTransactionData.getPollName().toLowerCase())) + return ValidationResult.NAME_NOT_LOWER_CASE; + + // Check the poll name isn't already taken + if (this.repository.getVotingRepository().pollExists(createPollTransactionData.getPollName())) + return ValidationResult.POLL_ALREADY_EXISTS; + + // XXX In gen1 we tested for votes but how can there be any if poll doesn't exist? + + // Check number of options + List pollOptions = createPollTransactionData.getPollOptions(); + int pollOptionsCount = pollOptions.size(); + if (pollOptionsCount < 1 || pollOptionsCount > MAX_OPTIONS) + return ValidationResult.INVALID_OPTIONS_COUNT; + + // Check each option + List optionNames = new ArrayList(); + for (PollOptionData pollOptionData : pollOptions) { + // Check option length + int optionNameLength = pollOptionData.getOptionName().getBytes(StandardCharsets.UTF_8).length; + if (optionNameLength < 1 || optionNameLength > MAX_NAME_SIZE) + return ValidationResult.INVALID_OPTION_LENGTH; + + // Check option is unique. NOTE: NOT case-sensitive! + if (optionNames.contains(pollOptionData.getOptionName())) { + return ValidationResult.DUPLICATE_OPTION; + } + + optionNames.add(pollOptionData.getOptionName()); + } + + // Check fee is positive + if (createPollTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + // Check reference is correct + PublicKeyAccount creator = new PublicKeyAccount(this.repository, createPollTransactionData.getCreatorPublicKey()); + + if (!Arrays.equals(creator.getLastReference(), createPollTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + // Check issuer has enough funds + if (creator.getConfirmedBalance(Asset.QORA).compareTo(createPollTransactionData.getFee()) == -1) + return ValidationResult.NO_BALANCE; + + return ValidationResult.OK; + } + + @Override + public void process() throws DataException { + // Publish poll to allow voting + Poll poll = new Poll(this.repository, createPollTransactionData); + poll.publish(); + + // Save this transaction, now with corresponding pollId + this.repository.getTransactionRepository().save(createPollTransactionData); + + // Update creator's balance + Account creator = new PublicKeyAccount(this.repository, createPollTransactionData.getCreatorPublicKey()); + creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(createPollTransactionData.getFee())); + + // Update creator's reference + creator.setLastReference(createPollTransactionData.getSignature()); + } + + @Override + public void orphan() throws DataException { + // Unpublish poll + Poll poll = new Poll(this.repository, createPollTransactionData.getPollName()); + poll.unpublish(); + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(createPollTransactionData); + + // Update issuer's balance + Account creator = new PublicKeyAccount(this.repository, createPollTransactionData.getCreatorPublicKey()); + creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(createPollTransactionData.getFee())); + + // Update issuer's reference + creator.setLastReference(createPollTransactionData.getReference()); + } + +} diff --git a/src/qora/transaction/IssueAssetTransaction.java b/src/qora/transaction/IssueAssetTransaction.java index 1bbdf190..080fe75b 100644 --- a/src/qora/transaction/IssueAssetTransaction.java +++ b/src/qora/transaction/IssueAssetTransaction.java @@ -5,7 +5,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import data.assets.AssetData; import data.transaction.IssueAssetTransactionData; import data.transaction.TransactionData; import qora.account.Account; @@ -15,14 +14,16 @@ import qora.block.BlockChain; import qora.crypto.Crypto; import repository.DataException; import repository.Repository; -import transform.transaction.IssueAssetTransactionTransformer; -import utils.NTP; public class IssueAssetTransaction extends Transaction { // Properties private IssueAssetTransactionData issueAssetTransactionData; + // Other useful constants + public static final int MAX_NAME_SIZE = 400; + public static final int MAX_DESCRIPTION_SIZE = 4000; + // Constructors public IssueAssetTransaction(Repository repository, TransactionData transactionData) { @@ -75,7 +76,8 @@ public class IssueAssetTransaction extends Transaction { public ValidationResult isValid() throws DataException { // Are IssueAssetTransactions even allowed at this point? - if (NTP.getTime() < BlockChain.ASSETS_RELEASE_TIMESTAMP) + // XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used? + if (this.issueAssetTransactionData.getTimestamp() < BlockChain.ASSETS_RELEASE_TIMESTAMP) return ValidationResult.NOT_YET_RELEASED; // Check owner address is valid @@ -83,13 +85,12 @@ public class IssueAssetTransaction extends Transaction { return ValidationResult.INVALID_ADDRESS; // Check name size bounds - if (issueAssetTransactionData.getAssetName().length() < 1 - || issueAssetTransactionData.getAssetName().length() > IssueAssetTransactionTransformer.MAX_NAME_SIZE) + if (issueAssetTransactionData.getAssetName().length() < 1 || issueAssetTransactionData.getAssetName().length() > IssueAssetTransaction.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; // Check description size bounds if (issueAssetTransactionData.getDescription().length() < 1 - || issueAssetTransactionData.getDescription().length() > IssueAssetTransactionTransformer.MAX_DESCRIPTION_SIZE) + || issueAssetTransactionData.getDescription().length() > IssueAssetTransaction.MAX_DESCRIPTION_SIZE) return ValidationResult.INVALID_DESCRIPTION_LENGTH; // Check quantity - either 10 billion or if that's not enough: a billion billion! @@ -120,13 +121,11 @@ public class IssueAssetTransaction extends Transaction { public void process() throws DataException { // Issue asset - AssetData assetData = new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(), - issueAssetTransactionData.getDescription(), issueAssetTransactionData.getQuantity(), issueAssetTransactionData.getIsDivisible(), - issueAssetTransactionData.getReference()); - this.repository.getAssetRepository().save(assetData); + Asset asset = new Asset(this.repository, issueAssetTransactionData); + asset.issue(); // Note newly assigned asset ID in our transaction record - issueAssetTransactionData.setAssetId(assetData.getAssetId()); + issueAssetTransactionData.setAssetId(asset.getAssetData().getAssetId()); // Save this transaction, now with corresponding assetId this.repository.getTransactionRepository().save(issueAssetTransactionData); @@ -148,8 +147,9 @@ public class IssueAssetTransaction extends Transaction { Account owner = new Account(this.repository, issueAssetTransactionData.getOwner()); owner.deleteBalance(issueAssetTransactionData.getAssetId()); - // Unissue asset - this.repository.getAssetRepository().delete(issueAssetTransactionData.getAssetId()); + // Issue asset + Asset asset = new Asset(this.repository, issueAssetTransactionData.getAssetId()); + asset.deissue(); // Delete this transaction itself this.repository.getTransactionRepository().delete(issueAssetTransactionData); diff --git a/src/qora/transaction/MessageTransaction.java b/src/qora/transaction/MessageTransaction.java index f1f957ed..43322566 100644 --- a/src/qora/transaction/MessageTransaction.java +++ b/src/qora/transaction/MessageTransaction.java @@ -21,8 +21,8 @@ public class MessageTransaction extends Transaction { // Properties private MessageTransactionData messageTransactionData; - // Useful constants - private static final int MAX_DATA_SIZE = 4000; + // Other useful constants + public static final int MAX_DATA_SIZE = 4000; // Constructors diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index 6014f500..978a5466 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -44,9 +44,10 @@ public abstract class Transaction { // Validation results 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( - 15), INVALID_DESCRIPTION_LENGTH(18), INVALID_DATA_LENGTH(27), INVALID_QUANTITY(28), ASSET_DOES_NOT_EXIST(29), INVALID_RETURN( - 30), HAVE_EQUALS_WANT(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); + 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( + 30), HAVE_EQUALS_WANT(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; @@ -87,6 +88,9 @@ public abstract class Transaction { case PAYMENT: return new PaymentTransaction(repository, transactionData); + case CREATE_POLL: + return new CreatePollTransaction(repository, transactionData); + case ISSUE_ASSET: return new IssueAssetTransaction(repository, transactionData); diff --git a/src/qora/voting/Poll.java b/src/qora/voting/Poll.java new file mode 100644 index 00000000..d548f35c --- /dev/null +++ b/src/qora/voting/Poll.java @@ -0,0 +1,53 @@ +package qora.voting; + +import data.transaction.CreatePollTransactionData; +import data.voting.PollData; +import repository.DataException; +import repository.Repository; + +public class Poll { + + // Properties + private Repository repository; + private PollData pollData; + + // Constructors + + public Poll(Repository repository, PollData pollData) { + this.repository = repository; + this.pollData = pollData; + } + + /** + * Create Poll business object using info from create poll transaction. + * + * @param repository + * @param createPollTransactionData + */ + public Poll(Repository repository, CreatePollTransactionData createPollTransactionData) { + this.repository = repository; + this.pollData = new PollData(createPollTransactionData.getCreatorPublicKey(), createPollTransactionData.getOwner(), + createPollTransactionData.getPollName(), createPollTransactionData.getDescription(), createPollTransactionData.getPollOptions()); + } + + public Poll(Repository repository, String pollName) throws DataException { + this.repository = repository; + this.pollData = this.repository.getVotingRepository().fromPollName(pollName); + } + + // Processing + + /** + * "Publish" poll to allow voting. + * + * @throws DataException + */ + public void publish() throws DataException { + this.repository.getVotingRepository().save(this.pollData); + } + + public void unpublish() throws DataException { + this.repository.getVotingRepository().delete(this.pollData.getPollName()); + } + +} diff --git a/src/repository/Repository.java b/src/repository/Repository.java index 46685e59..656a5d06 100644 --- a/src/repository/Repository.java +++ b/src/repository/Repository.java @@ -10,6 +10,8 @@ public interface Repository extends AutoCloseable { public TransactionRepository getTransactionRepository(); + public VotingRepository getVotingRepository(); + public void saveChanges() throws DataException; public void discardChanges() throws DataException; diff --git a/src/repository/VotingRepository.java b/src/repository/VotingRepository.java new file mode 100644 index 00000000..75a30642 --- /dev/null +++ b/src/repository/VotingRepository.java @@ -0,0 +1,15 @@ +package repository; + +import data.voting.PollData; + +public interface VotingRepository { + + public PollData fromPollName(String pollName) throws DataException; + + public boolean pollExists(String pollName) throws DataException; + + public void save(PollData pollData) throws DataException; + + public void delete(String pollName) throws DataException; + +} diff --git a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java index fd6b0bc3..71c5a70f 100644 --- a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -183,8 +183,8 @@ public class HSQLDBDatabaseUpdates { case 10: // Create Poll Transactions - stmt.execute("CREATE TABLE CreatePollTransactions (signature Signature, creator QoraPublicKey NOT NULL, poll PollName NOT NULL, " - + "description VARCHAR(4000) NOT NULL, " + stmt.execute("CREATE TABLE CreatePollTransactions (signature Signature, creator QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, " + + "poll PollName NOT NULL, description VARCHAR(4000) NOT NULL, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); // Poll options. NB: option is implicitly NON NULL and UNIQUE due to being part of compound primary key stmt.execute("CREATE TABLE CreatePollTransactionOptions (signature Signature, option PollOption, " @@ -270,7 +270,7 @@ public class HSQLDBDatabaseUpdates { case 21: // Assets (including QORA coin itself) stmt.execute( - "CREATE TABLE Assets (asset_id AssetID IDENTITY, owner QoraPublicKey NOT NULL, asset_name AssetName NOT NULL, description VARCHAR(4000) NOT NULL, " + "CREATE TABLE Assets (asset_id AssetID IDENTITY, owner QoraAddress NOT NULL, asset_name AssetName NOT NULL, description VARCHAR(4000) NOT NULL, " + "quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, reference Signature NOT NULL)"); stmt.execute("CREATE INDEX AssetNameIndex on Assets (asset_name)"); break; diff --git a/src/repository/hsqldb/HSQLDBRepository.java b/src/repository/hsqldb/HSQLDBRepository.java index 7864a5fd..46390f97 100644 --- a/src/repository/hsqldb/HSQLDBRepository.java +++ b/src/repository/hsqldb/HSQLDBRepository.java @@ -15,6 +15,7 @@ import repository.BlockRepository; import repository.DataException; import repository.Repository; import repository.TransactionRepository; +import repository.VotingRepository; import repository.hsqldb.transaction.HSQLDBTransactionRepository; public class HSQLDBRepository implements Repository { @@ -46,6 +47,11 @@ public class HSQLDBRepository implements Repository { return new HSQLDBTransactionRepository(this); } + @Override + public VotingRepository getVotingRepository() { + return new HSQLDBVotingRepository(this); + } + @Override public void saveChanges() throws DataException { try { diff --git a/src/repository/hsqldb/HSQLDBVotingRepository.java b/src/repository/hsqldb/HSQLDBVotingRepository.java new file mode 100644 index 00000000..0cb6050f --- /dev/null +++ b/src/repository/hsqldb/HSQLDBVotingRepository.java @@ -0,0 +1,33 @@ +package repository.hsqldb; + +import data.voting.PollData; +import repository.VotingRepository; +import repository.DataException; + +public class HSQLDBVotingRepository implements VotingRepository { + + protected HSQLDBRepository repository; + + public HSQLDBVotingRepository(HSQLDBRepository repository) { + this.repository = repository; + } + + public PollData fromPollName(String pollName) throws DataException { + // TODO + return null; + } + + public boolean pollExists(String pollName) throws DataException { + // TODO + return false; + } + + public void save(PollData pollData) throws DataException { + // TODO + } + + public void delete(String pollName) throws DataException { + // TODO + } + +} diff --git a/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java new file mode 100644 index 00000000..24622d5d --- /dev/null +++ b/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java @@ -0,0 +1,81 @@ +package repository.hsqldb.transaction; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import data.transaction.CreatePollTransactionData; +import data.transaction.TransactionData; +import data.voting.PollOptionData; +import repository.DataException; +import repository.hsqldb.HSQLDBRepository; +import repository.hsqldb.HSQLDBSaver; + +public class HSQLDBCreatePollTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBCreatePollTransactionRepository(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 owner, poll_name, description FROM CreatePollTransactions WHERE signature = ?", signature); + if (rs == null) + return null; + + String owner = rs.getString(1); + String pollName = rs.getString(2); + String description = rs.getString(3); + + rs = this.repository.checkedExecute("SELECT option_name FROM CreatePollTransactionOptions where signature = ?", signature); + if (rs == null) + return null; + + List pollOptions = new ArrayList(); + + // NOTE: do-while because checkedExecute() above has already called rs.next() for us + do { + String optionName = rs.getString(1); + + pollOptions.add(new PollOptionData(optionName)); + } while (rs.next()); + + return new CreatePollTransactionData(creatorPublicKey, owner, pollName, description, pollOptions, fee, timestamp, reference, signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch create poll transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + CreatePollTransactionData createPollTransactionData = (CreatePollTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("CreatePollTransactions"); + + saveHelper.bind("signature", createPollTransactionData.getSignature()).bind("creator", createPollTransactionData.getCreatorPublicKey()) + .bind("owner", createPollTransactionData.getOwner()).bind("poll_name", createPollTransactionData.getPollName()) + .bind("description", createPollTransactionData.getDescription()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save create poll transaction into repository", e); + } + + // Now attempt to save poll options + for (PollOptionData pollOptionData : createPollTransactionData.getPollOptions()) { + HSQLDBSaver optionSaveHelper = new HSQLDBSaver("CreatePollTransactionOptions"); + + optionSaveHelper.bind("signature", createPollTransactionData.getSignature()).bind("option_name", pollOptionData.getOptionName()); + + try { + optionSaveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save create poll transaction option into repository", e); + } + } + } + +} diff --git a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index b99e81ae..39cb3aa6 100644 --- a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -21,6 +21,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { protected HSQLDBRepository repository; private HSQLDBGenesisTransactionRepository genesisTransactionRepository; private HSQLDBPaymentTransactionRepository paymentTransactionRepository; + private HSQLDBCreatePollTransactionRepository createPollTransactionRepository; private HSQLDBIssueAssetTransactionRepository issueAssetTransactionRepository; private HSQLDBTransferAssetTransactionRepository transferAssetTransactionRepository; private HSQLDBCreateOrderTransactionRepository createOrderTransactionRepository; @@ -32,6 +33,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.repository = repository; this.genesisTransactionRepository = new HSQLDBGenesisTransactionRepository(repository); this.paymentTransactionRepository = new HSQLDBPaymentTransactionRepository(repository); + this.createPollTransactionRepository = new HSQLDBCreatePollTransactionRepository(repository); this.issueAssetTransactionRepository = new HSQLDBIssueAssetTransactionRepository(repository); this.transferAssetTransactionRepository = new HSQLDBTransferAssetTransactionRepository(repository); this.createOrderTransactionRepository = new HSQLDBCreateOrderTransactionRepository(repository); @@ -88,6 +90,9 @@ public class HSQLDBTransactionRepository implements TransactionRepository { case PAYMENT: return this.paymentTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case CREATE_POLL: + return this.createPollTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case ISSUE_ASSET: return this.issueAssetTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); @@ -210,6 +215,10 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.paymentTransactionRepository.save(transactionData); break; + case CREATE_POLL: + this.createPollTransactionRepository.save(transactionData); + break; + case ISSUE_ASSET: this.issueAssetTransactionRepository.save(transactionData); break; @@ -224,9 +233,11 @@ public class HSQLDBTransactionRepository implements TransactionRepository { case CANCEL_ASSET_ORDER: this.cancelOrderTransactionRepository.save(transactionData); + break; case MULTIPAYMENT: this.multiPaymentTransactionRepository.save(transactionData); + break; case MESSAGE: this.messageTransactionRepository.save(transactionData); diff --git a/src/transform/transaction/CreatePollTransactionTransformer.java b/src/transform/transaction/CreatePollTransactionTransformer.java new file mode 100644 index 00000000..fe30cbfa --- /dev/null +++ b/src/transform/transaction/CreatePollTransactionTransformer.java @@ -0,0 +1,151 @@ +package transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.json.simple.JSONArray; +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.CreatePollTransactionData; +import data.transaction.TransactionData; +import data.voting.PollOptionData; +import qora.account.PublicKeyAccount; +import qora.transaction.CreatePollTransaction; +import transform.TransformationException; +import utils.Base58; +import utils.Serialization; + +public class CreatePollTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int OWNER_LENGTH = ADDRESS_LENGTH; + private static final int NAME_SIZE_LENGTH = INT_LENGTH; + private static final int DESCRIPTION_SIZE_LENGTH = INT_LENGTH; + private static final int OPTIONS_SIZE_LENGTH = INT_LENGTH; + + private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + OWNER_LENGTH + NAME_SIZE_LENGTH + DESCRIPTION_SIZE_LENGTH; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + if (byteBuffer.remaining() < TYPELESS_DATALESS_LENGTH) + throw new TransformationException("Byte data too short for CreatePollTransaction"); + + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] creatorPublicKey = Serialization.deserializePublicKey(byteBuffer); + + String owner = Serialization.deserializeRecipient(byteBuffer); + + String pollName = Serialization.deserializeSizedString(byteBuffer, CreatePollTransaction.MAX_NAME_SIZE); + String description = Serialization.deserializeSizedString(byteBuffer, CreatePollTransaction.MAX_DESCRIPTION_SIZE); + + // Make sure there are enough bytes left for poll options + if (byteBuffer.remaining() < OPTIONS_SIZE_LENGTH) + throw new TransformationException("Byte data too short for CreatePollTransaction"); + + int optionsCount = byteBuffer.getInt(); + if (optionsCount < 1 || optionsCount > CreatePollTransaction.MAX_OPTIONS) + throw new TransformationException("Invalid number of options for CreatePollTransaction"); + + List pollOptions = new ArrayList(); + for (int i = 0; i < optionsCount; ++i) { + String optionName = Serialization.deserializeSizedString(byteBuffer, CreatePollTransaction.MAX_NAME_SIZE); + + pollOptions.add(new PollOptionData(optionName)); + } + + // 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 CreatePollTransaction"); + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new CreatePollTransactionData(creatorPublicKey, owner, pollName, description, pollOptions, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + CreatePollTransactionData createPollTransactionData = (CreatePollTransactionData) transactionData; + + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + createPollTransactionData.getPollName().length(); + + // Add lengths for each poll options + for (PollOptionData pollOptionData : createPollTransactionData.getPollOptions()) + dataLength += OPTIONS_SIZE_LENGTH + pollOptionData.getOptionName().length(); + + return dataLength; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + CreatePollTransactionData createPollTransactionData = (CreatePollTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(createPollTransactionData.getType().value)); + bytes.write(Longs.toByteArray(createPollTransactionData.getTimestamp())); + bytes.write(createPollTransactionData.getReference()); + + bytes.write(createPollTransactionData.getCreatorPublicKey()); + bytes.write(Base58.decode(createPollTransactionData.getOwner())); + Serialization.serializeSizedString(bytes, createPollTransactionData.getPollName()); + Serialization.serializeSizedString(bytes, createPollTransactionData.getDescription()); + + List pollOptions = createPollTransactionData.getPollOptions(); + bytes.write(Ints.toByteArray(pollOptions.size())); + + for (PollOptionData pollOptionData : pollOptions) { + Serialization.serializeSizedString(bytes, pollOptionData.getOptionName()); + } + + Serialization.serializeBigDecimal(bytes, createPollTransactionData.getFee()); + bytes.write(createPollTransactionData.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 { + CreatePollTransactionData createPollTransactionData = (CreatePollTransactionData) transactionData; + + byte[] creatorPublicKey = createPollTransactionData.getCreatorPublicKey(); + + json.put("creator", PublicKeyAccount.getAddress(creatorPublicKey)); + json.put("creatorPublicKey", HashCode.fromBytes(creatorPublicKey).toString()); + + json.put("owner", createPollTransactionData.getOwner()); + json.put("name", createPollTransactionData.getPollName()); + json.put("description", createPollTransactionData.getDescription()); + + JSONArray options = new JSONArray(); + for (PollOptionData optionData : createPollTransactionData.getPollOptions()) { + options.add(optionData.getOptionName()); + } + + json.put("options", options); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/transform/transaction/IssueAssetTransactionTransformer.java b/src/transform/transaction/IssueAssetTransactionTransformer.java index dcf4364e..d7693bf6 100644 --- a/src/transform/transaction/IssueAssetTransactionTransformer.java +++ b/src/transform/transaction/IssueAssetTransactionTransformer.java @@ -13,6 +13,7 @@ import com.google.common.primitives.Longs; import data.transaction.TransactionData; import qora.account.PublicKeyAccount; +import qora.transaction.IssueAssetTransaction; import data.transaction.IssueAssetTransactionData; import transform.TransformationException; import utils.Base58; @@ -31,10 +32,6 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + ISSUER_LENGTH + OWNER_LENGTH + NAME_SIZE_LENGTH + DESCRIPTION_SIZE_LENGTH + QUANTITY_LENGTH + IS_DIVISIBLE_LENGTH; - // Other useful lengths - public static final int MAX_NAME_SIZE = 400; - public static final int MAX_DESCRIPTION_SIZE = 4000; - static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { if (byteBuffer.remaining() < TYPELESS_LENGTH) throw new TransformationException("Byte data too short for IssueAssetTransaction"); @@ -47,11 +44,11 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { byte[] issuerPublicKey = Serialization.deserializePublicKey(byteBuffer); String owner = Serialization.deserializeRecipient(byteBuffer); - String assetName = Serialization.deserializeSizedString(byteBuffer, MAX_NAME_SIZE); - String description = Serialization.deserializeSizedString(byteBuffer, MAX_DESCRIPTION_SIZE); + String assetName = Serialization.deserializeSizedString(byteBuffer, IssueAssetTransaction.MAX_NAME_SIZE); + String description = Serialization.deserializeSizedString(byteBuffer, IssueAssetTransaction.MAX_DESCRIPTION_SIZE); // Still need to make sure there are enough bytes left for remaining fields - if (byteBuffer.remaining() < QUANTITY_LENGTH + IS_DIVISIBLE_LENGTH + SIGNATURE_LENGTH) + if (byteBuffer.remaining() < QUANTITY_LENGTH + IS_DIVISIBLE_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH) throw new TransformationException("Byte data too short for IssueAssetTransaction"); long quantity = byteBuffer.getLong(); diff --git a/src/transform/transaction/MessageTransactionTransformer.java b/src/transform/transaction/MessageTransactionTransformer.java index e3071317..20cdaccb 100644 --- a/src/transform/transaction/MessageTransactionTransformer.java +++ b/src/transform/transaction/MessageTransactionTransformer.java @@ -37,9 +37,6 @@ public class MessageTransactionTransformer extends TransactionTransformer { private static final int TYPELESS_DATALESS_LENGTH_V3 = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + ASSET_ID_LENGTH + AMOUNT_LENGTH + DATA_SIZE_LENGTH + IS_TEXT_LENGTH + IS_ENCRYPTED_LENGTH; - // Other property lengths - private static final int MAX_DATA_SIZE = 4000; - static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { if (byteBuffer.remaining() < TYPELESS_DATALESS_LENGTH_V1) throw new TransformationException("Byte data too short for MessageTransaction"); @@ -69,12 +66,16 @@ public class MessageTransactionTransformer extends TransactionTransformer { int dataSize = byteBuffer.getInt(0); // Don't allow invalid dataSize here to avoid run-time issues - if (dataSize > MAX_DATA_SIZE) + if (dataSize > MessageTransaction.MAX_DATA_SIZE) throw new TransformationException("MessageTransaction data size too large"); byte[] data = new byte[dataSize]; byteBuffer.get(data); + // Still need to make sure there are enough bytes left for remaining fields + if (byteBuffer.remaining() < IS_ENCRYPTED_LENGTH + IS_TEXT_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH) + throw new TransformationException("Byte data too short for MessageTransaction"); + boolean isEncrypted = byteBuffer.get() != 0; boolean isText = byteBuffer.get() != 0; diff --git a/src/transform/transaction/TransactionTransformer.java b/src/transform/transaction/TransactionTransformer.java index dd4586bd..bc3b80eb 100644 --- a/src/transform/transaction/TransactionTransformer.java +++ b/src/transform/transaction/TransactionTransformer.java @@ -37,6 +37,9 @@ public class TransactionTransformer extends Transformer { case PAYMENT: return PaymentTransactionTransformer.fromByteBuffer(byteBuffer); + case CREATE_POLL: + return CreatePollTransactionTransformer.fromByteBuffer(byteBuffer); + case ISSUE_ASSET: return IssueAssetTransactionTransformer.fromByteBuffer(byteBuffer); @@ -68,6 +71,9 @@ public class TransactionTransformer extends Transformer { case PAYMENT: return PaymentTransactionTransformer.getDataLength(transactionData); + case CREATE_POLL: + return CreatePollTransactionTransformer.getDataLength(transactionData); + case ISSUE_ASSET: return IssueAssetTransactionTransformer.getDataLength(transactionData); @@ -99,6 +105,9 @@ public class TransactionTransformer extends Transformer { case PAYMENT: return PaymentTransactionTransformer.toBytes(transactionData); + case CREATE_POLL: + return CreatePollTransactionTransformer.toBytes(transactionData); + case ISSUE_ASSET: return IssueAssetTransactionTransformer.toBytes(transactionData); @@ -130,6 +139,9 @@ public class TransactionTransformer extends Transformer { case PAYMENT: return PaymentTransactionTransformer.toJSON(transactionData); + case CREATE_POLL: + return CreatePollTransactionTransformer.toJSON(transactionData); + case ISSUE_ASSET: return IssueAssetTransactionTransformer.toJSON(transactionData); diff --git a/src/utils/Serialization.java b/src/utils/Serialization.java index 5672cf47..ea0c4c5a 100644 --- a/src/utils/Serialization.java +++ b/src/utils/Serialization.java @@ -68,10 +68,16 @@ public class Serialization { } public static String deserializeSizedString(ByteBuffer byteBuffer, int maxSize) throws TransformationException { + if (byteBuffer.remaining() < Transformer.INT_LENGTH) + throw new TransformationException("Byte data too short for serialized string size"); + int size = byteBuffer.getInt(); - if (size > maxSize || size > byteBuffer.remaining()) + if (size > maxSize) throw new TransformationException("Serialized string too long"); + if (size > byteBuffer.remaining()) + throw new TransformationException("Byte data too short for serialized string"); + byte[] bytes = new byte[size]; byteBuffer.get(bytes);