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.
This commit is contained in:
catbref 2018-06-19 12:30:17 +01:00
parent c5a32ffa1c
commit 5e674cbaab
22 changed files with 740 additions and 35 deletions

View File

@ -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<PollOptionData> pollOptions;
// Constructors
public CreatePollTransactionData(byte[] creatorPublicKey, String owner, String pollName, String description, List<PollOptionData> 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<PollOptionData> getPollOptions() {
return this.pollOptions;
}
}

View File

@ -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<PollOptionData> pollOptions;
// Constructors
public PollData(byte[] creatorPublicKey, String owner, String pollName, String description, List<PollOptionData> 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<PollOptionData> getPollOptions() {
return this.pollOptions;
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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.

View File

@ -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<Account> 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<PollOptionData> pollOptions = createPollTransactionData.getPollOptions();
int pollOptionsCount = pollOptions.size();
if (pollOptionsCount < 1 || pollOptionsCount > MAX_OPTIONS)
return ValidationResult.INVALID_OPTIONS_COUNT;
// Check each option
List<String> optionNames = new ArrayList<String>();
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());
}
}

View File

@ -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);

View File

@ -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

View File

@ -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);

53
src/qora/voting/Poll.java Normal file
View File

@ -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());
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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<PollOptionData> pollOptions = new ArrayList<PollOptionData>();
// 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);
}
}
}
}

View File

@ -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);

View File

@ -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<PollOptionData> pollOptions = new ArrayList<PollOptionData>();
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<PollOptionData> 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;
}
}

View File

@ -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();

View File

@ -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;

View File

@ -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);

View File

@ -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);