diff --git a/src/data/transaction/CreatePollTransactionData.java b/src/data/transaction/CreatePollTransactionData.java index df00c896..4d6cd111 100644 --- a/src/data/transaction/CreatePollTransactionData.java +++ b/src/data/transaction/CreatePollTransactionData.java @@ -28,6 +28,11 @@ public class CreatePollTransactionData extends TransactionData { this.pollOptions = pollOptions; } + public CreatePollTransactionData(byte[] creatorPublicKey, String owner, String pollName, String description, List pollOptions, + BigDecimal fee, long timestamp, byte[] reference) { + this(creatorPublicKey, owner, pollName, description, pollOptions, fee, timestamp, reference, null); + } + // Getters/setters public byte[] getCreatorPublicKey() { diff --git a/src/data/voting/PollData.java b/src/data/voting/PollData.java index eab99ee1..98f65b67 100644 --- a/src/data/voting/PollData.java +++ b/src/data/voting/PollData.java @@ -12,14 +12,17 @@ public class PollData { private String pollName; private String description; private List pollOptions; + private long published; // Constructors - public PollData(byte[] creatorPublicKey, String owner, String pollName, String description, List pollOptions) { + public PollData(byte[] creatorPublicKey, String owner, String pollName, String description, List pollOptions, long published) { this.creatorPublicKey = creatorPublicKey; + this.owner = owner; this.pollName = pollName; this.description = description; this.pollOptions = pollOptions; + this.published = published; } // Getters/setters @@ -44,4 +47,12 @@ public class PollData { return this.pollOptions; } + public long getPublished() { + return this.published; + } + + public void setPublished(long published) { + this.published = published; + } + } diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index c7bbc594..7ef9651c 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -483,8 +483,7 @@ public class Block { Block parentBlock = new Block(this.repository, parentBlockData); // Check timestamp is newer than parent timestamp - if (this.blockData.getTimestamp() <= parentBlockData.getTimestamp() - || this.blockData.getTimestamp() - BlockChain.BLOCK_TIMESTAMP_MARGIN > NTP.getTime()) + if (this.blockData.getTimestamp() <= parentBlockData.getTimestamp()) return ValidationResult.TIMESTAMP_OLDER_THAN_PARENT; // Check timestamp is not in the future (within configurable ~500ms margin) diff --git a/src/qora/block/GenesisBlock.java b/src/qora/block/GenesisBlock.java index e816556f..10c2b77e 100644 --- a/src/qora/block/GenesisBlock.java +++ b/src/qora/block/GenesisBlock.java @@ -17,6 +17,7 @@ import qora.crypto.Crypto; import qora.transaction.Transaction; import repository.DataException; import repository.Repository; +import utils.NTP; public class GenesisBlock extends Block { diff --git a/src/qora/voting/Poll.java b/src/qora/voting/Poll.java index d548f35c..d8837bc7 100644 --- a/src/qora/voting/Poll.java +++ b/src/qora/voting/Poll.java @@ -27,7 +27,7 @@ public class Poll { public Poll(Repository repository, CreatePollTransactionData createPollTransactionData) { this.repository = repository; this.pollData = new PollData(createPollTransactionData.getCreatorPublicKey(), createPollTransactionData.getOwner(), - createPollTransactionData.getPollName(), createPollTransactionData.getDescription(), createPollTransactionData.getPollOptions()); + createPollTransactionData.getPollName(), createPollTransactionData.getDescription(), createPollTransactionData.getPollOptions(), createPollTransactionData.getTimestamp()); } public Poll(Repository repository, String pollName) throws DataException { diff --git a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java index 71c5a70f..b94b051f 100644 --- a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -24,6 +24,7 @@ public class HSQLDBDatabaseUpdates { */ private static void incrementDatabaseVersion(Connection connection) throws SQLException { connection.createStatement().execute("UPDATE DatabaseInfo SET version = version + 1"); + connection.commit(); } /** @@ -101,9 +102,13 @@ public class HSQLDBDatabaseUpdates { + "transaction_count INTEGER NOT NULL, total_fees QoraAmount NOT NULL, transactions_signature Signature NOT NULL, " + "height INTEGER NOT NULL, generation TIMESTAMP NOT NULL, generating_balance QoraAmount NOT NULL, " + "generator QoraPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_data VARBINARY(20000), AT_fees QoraAmount)"); + // For finding blocks by height. stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)"); + // For finding blocks by the account that generated them. stmt.execute("CREATE INDEX BlockGeneratorIndex ON Blocks (generator)"); + // For finding blocks by reference, e.g. child blocks. stmt.execute("CREATE INDEX BlockReferenceIndex ON Blocks (reference)"); + // Use a separate table space as this table will be very large. stmt.execute("SET TABLE Blocks NEW SPACE"); break; @@ -111,26 +116,33 @@ public class HSQLDBDatabaseUpdates { // Generic transactions (null reference, creator and milestone_block for genesis transactions) stmt.execute("CREATE TABLE Transactions (signature Signature PRIMARY KEY, reference Signature, type TINYINT NOT NULL, " + "creator QoraPublicKey, creation TIMESTAMP NOT NULL, fee QoraAmount NOT NULL, milestone_block BlockSignature)"); + // For finding transactions by transaction type. stmt.execute("CREATE INDEX TransactionTypeIndex ON Transactions (type)"); + // For finding transactions using timestamp. stmt.execute("CREATE INDEX TransactionCreationIndex ON Transactions (creation)"); + // For when a user wants to lookup ALL transactions they have created, regardless of type. stmt.execute("CREATE INDEX TransactionCreatorIndex ON Transactions (creator)"); + // For finding transactions by reference, e.g. child transactions. stmt.execute("CREATE INDEX TransactionReferenceIndex ON Transactions (reference)"); + // Use a separate table space as this table will be very large. stmt.execute("SET TABLE Transactions NEW SPACE"); // Transaction-Block mapping ("signature" is unique as a transaction cannot be included in more than one block) stmt.execute("CREATE TABLE BlockTransactions (block_signature BlockSignature, sequence INTEGER, transaction_signature Signature, " + "PRIMARY KEY (block_signature, sequence), FOREIGN KEY (transaction_signature) REFERENCES Transactions (signature) ON DELETE CASCADE, " + "FOREIGN KEY (block_signature) REFERENCES Blocks (signature) ON DELETE CASCADE)"); + // Use a separate table space as this table will be very large. stmt.execute("SET TABLE BlockTransactions NEW SPACE"); // Unconfirmed transactions - // Do we need this? If a transaction doesn't have a corresponding BlockTransactions record then it's unconfirmed? + // XXX Do we need this? If a transaction doesn't have a corresponding BlockTransactions record then it's unconfirmed? stmt.execute("CREATE TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, expiry TIMESTAMP NOT NULL)"); stmt.execute("CREATE INDEX UnconfirmedTransactionExpiryIndex ON UnconfirmedTransactions (expiry)"); // Transaction recipients stmt.execute("CREATE TABLE TransactionRecipients (signature Signature, recipient QoraAddress NOT NULL, " + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Use a separate table space as this table will be very large. stmt.execute("SET TABLE TransactionRecipients NEW SPACE"); break; @@ -184,11 +196,11 @@ public class HSQLDBDatabaseUpdates { case 10: // Create Poll Transactions stmt.execute("CREATE TABLE CreatePollTransactions (signature Signature, creator QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, " - + "poll PollName NOT NULL, description VARCHAR(4000) NOT NULL, " + + "poll_name 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, " - + "PRIMARY KEY (signature, option), FOREIGN KEY (signature) REFERENCES CreatePollTransactions (signature) ON DELETE CASCADE)"); + stmt.execute("CREATE TABLE CreatePollTransactionOptions (signature Signature, option_name PollOption, " + + "PRIMARY KEY (signature, option_name), FOREIGN KEY (signature) REFERENCES CreatePollTransactions (signature) ON DELETE CASCADE)"); // For the future: add flag to polls to allow one or multiple votes per voter break; @@ -272,6 +284,7 @@ public class HSQLDBDatabaseUpdates { stmt.execute( "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)"); + // For when a user wants to lookup an asset by name stmt.execute("CREATE INDEX AssetNameIndex on Assets (asset_name)"); break; @@ -288,8 +301,26 @@ public class HSQLDBDatabaseUpdates { "CREATE TABLE AssetOrders (asset_order_id AssetOrderID, creator QoraPublicKey NOT NULL, have_asset_id AssetID NOT NULL, want_asset_id AssetID NOT NULL, " + "amount QoraAmount NOT NULL, fulfilled QoraAmount NOT NULL, price QoraAmount NOT NULL, ordered TIMESTAMP NOT NULL, is_closed BOOLEAN NOT NULL, " + "PRIMARY KEY (asset_order_id))"); + // For quick matching of orders. is_closed included so inactive orders can be filtered out. stmt.execute("CREATE INDEX AssetOrderHaveIndex on AssetOrders (have_asset_id, is_closed)"); stmt.execute("CREATE INDEX AssetOrderWantIndex on AssetOrders (want_asset_id, is_closed)"); + // For when a user wants to look up their current/historic orders. is_closed included so user can filter by active/inactive orders. + stmt.execute("CREATE INDEX AssetOrderCreatorIndex on AssetOrders (creator, is_closed)"); + break; + + case 24: + // Polls/Voting + stmt.execute( + "CREATE TABLE Polls (poll_name PollName, description VARCHAR(4000) NOT NULL, creator QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, " + + "published TIMESTAMP NOT NULL, " + "PRIMARY KEY (poll_name))"); + // Various options available on a poll + stmt.execute("CREATE TABLE PollOptions (poll_name PollName, option_name PollOption, " + + "PRIMARY KEY (poll_name, option_name), FOREIGN KEY (poll_name) REFERENCES Polls (poll_name) ON DELETE CASCADE)"); + // Actual votes cast on a poll by voting users. NOTE: only one vote per user supported at this time. + stmt.execute("CREATE TABLE PollVotes (poll_name PollName, voter QoraPublicKey, option_name PollOption, " + + "PRIMARY KEY (poll_name, voter), FOREIGN KEY (poll_name) REFERENCES Polls (poll_name) ON DELETE CASCADE)"); + // For when a user wants to lookup poll they own + stmt.execute("CREATE INDEX PollOwnerIndex on Polls (owner)"); break; default: diff --git a/src/repository/hsqldb/HSQLDBRepository.java b/src/repository/hsqldb/HSQLDBRepository.java index 5f9be839..e9097173 100644 --- a/src/repository/hsqldb/HSQLDBRepository.java +++ b/src/repository/hsqldb/HSQLDBRepository.java @@ -112,15 +112,7 @@ public class HSQLDBRepository implements Repository { public ResultSet checkedExecute(String sql, Object... objects) throws SQLException { PreparedStatement preparedStatement = this.connection.prepareStatement(sql); - for (int i = 0; i < objects.length; ++i) - // Special treatment for BigDecimals so that they retain their "scale", - // which would otherwise be assumed as 0. - if (objects[i] instanceof BigDecimal) - preparedStatement.setBigDecimal(i + 1, (BigDecimal) objects[i]); - else - preparedStatement.setObject(i + 1, objects[i]); - - return this.checkedExecute(preparedStatement); + return this.checkedExecute(preparedStatement, objects); } /** @@ -129,10 +121,19 @@ public class HSQLDBRepository implements Repository { * Note: calls ResultSet.next() therefore returned ResultSet is already pointing to first row. * * @param preparedStatement + * @param objects * @return ResultSet, or null if there are no found rows * @throws SQLException */ - public ResultSet checkedExecute(PreparedStatement preparedStatement) throws SQLException { + public ResultSet checkedExecute(PreparedStatement preparedStatement, Object... objects) throws SQLException { + for (int i = 0; i < objects.length; ++i) + // Special treatment for BigDecimals so that they retain their "scale", + // which would otherwise be assumed as 0. + if (objects[i] instanceof BigDecimal) + preparedStatement.setBigDecimal(i + 1, (BigDecimal) objects[i]); + else + preparedStatement.setObject(i + 1, objects[i]); + if (!preparedStatement.execute()) throw new SQLException("Fetching from database produced no results"); @@ -183,9 +184,8 @@ public class HSQLDBRepository implements Repository { * @throws SQLException */ public boolean exists(String tableName, String whereClause, Object... objects) throws SQLException { - PreparedStatement preparedStatement = this.connection - .prepareStatement("SELECT TRUE FROM " + tableName + " WHERE " + whereClause + " ORDER BY NULL LIMIT 1"); - ResultSet resultSet = this.checkedExecute(preparedStatement); + PreparedStatement preparedStatement = this.connection.prepareStatement("SELECT TRUE FROM " + tableName + " WHERE " + whereClause + " LIMIT 1"); + ResultSet resultSet = this.checkedExecute(preparedStatement, objects); if (resultSet == null) return false; diff --git a/src/repository/hsqldb/HSQLDBVotingRepository.java b/src/repository/hsqldb/HSQLDBVotingRepository.java index 0cb6050f..67c5b653 100644 --- a/src/repository/hsqldb/HSQLDBVotingRepository.java +++ b/src/repository/hsqldb/HSQLDBVotingRepository.java @@ -1,6 +1,13 @@ package repository.hsqldb; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + import data.voting.PollData; +import data.voting.PollOptionData; import repository.VotingRepository; import repository.DataException; @@ -13,21 +20,77 @@ public class HSQLDBVotingRepository implements VotingRepository { } public PollData fromPollName(String pollName) throws DataException { - // TODO - return null; + try { + ResultSet resultSet = this.repository.checkedExecute("SELECT description, creator, owner, published FROM Polls WHERE poll_name = ?", pollName); + if (resultSet == null) + return null; + + String description = resultSet.getString(1); + byte[] creatorPublicKey = resultSet.getBytes(2); + String owner = resultSet.getString(3); + long published = resultSet.getTimestamp(4).getTime(); + + resultSet = this.repository.checkedExecute("SELECT option_name FROM PollOptions where poll_name = ?", pollName); + if (resultSet == null) + return null; + + List pollOptions = new ArrayList(); + + // NOTE: do-while because checkedExecute() above has already called rs.next() for us + do { + String optionName = resultSet.getString(1); + + pollOptions.add(new PollOptionData(optionName)); + } while (resultSet.next()); + + return new PollData(creatorPublicKey, owner, pollName, description, pollOptions, published); + } catch (SQLException e) { + throw new DataException("Unable to fetch poll from repository", e); + } } public boolean pollExists(String pollName) throws DataException { - // TODO - return false; + try { + return this.repository.exists("Polls", "poll_name = ?", pollName); + } catch (SQLException e) { + throw new DataException("Unable to check for poll in repository", e); + } } public void save(PollData pollData) throws DataException { - // TODO + HSQLDBSaver saveHelper = new HSQLDBSaver("Polls"); + + saveHelper.bind("poll_name", pollData.getPollName()).bind("description", pollData.getDescription()).bind("creator", pollData.getCreatorPublicKey()) + .bind("owner", pollData.getOwner()).bind("published", new Timestamp(pollData.getPublished())); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save poll into repository", e); + } + + // Now attempt to save poll options + for (PollOptionData pollOptionData : pollData.getPollOptions()) { + HSQLDBSaver optionSaveHelper = new HSQLDBSaver("PollOptions"); + + optionSaveHelper.bind("poll_name", pollData.getPollName()).bind("option_name", pollOptionData.getOptionName()); + + try { + optionSaveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save poll option into repository", e); + } + } } public void delete(String pollName) throws DataException { - // TODO + // NOTE: The corresponding rows in PollOptions are deleted automatically by the database thanks to "ON DELETE CASCADE" in the PollOptions' FOREIGN KEY + // definition. + try { + this.repository.checkedExecute("DELETE FROM Polls WHERE poll_name = ?", pollName); + } catch (SQLException e) { + throw new DataException("Unable to delete poll from repository", e); + } } } diff --git a/src/test/TransactionTests.java b/src/test/TransactionTests.java index a33ea64a..2323a28a 100644 --- a/src/test/TransactionTests.java +++ b/src/test/TransactionTests.java @@ -3,8 +3,12 @@ package test; import static org.junit.Assert.*; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import org.junit.After; import org.junit.AfterClass; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -13,13 +17,17 @@ import com.google.common.hash.HashCode; import data.account.AccountBalanceData; import data.account.AccountData; import data.block.BlockData; +import data.transaction.CreatePollTransactionData; import data.transaction.PaymentTransactionData; +import data.voting.PollData; +import data.voting.PollOptionData; import qora.account.Account; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; import qora.assets.Asset; import qora.block.Block; import qora.block.BlockChain; +import qora.transaction.CreatePollTransaction; import qora.transaction.PaymentTransaction; import qora.transaction.Transaction; import qora.transaction.Transaction.ValidationResult; @@ -39,19 +47,21 @@ public class TransactionTests { private static final byte[] senderSeed = HashCode.fromString("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef").asBytes(); private static final byte[] recipientSeed = HashCode.fromString("fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210").asBytes(); - @BeforeClass - public static void setRepository() throws DataException { + private static final BigDecimal generatorBalance = BigDecimal.valueOf(1_000_000_000L); + private static final BigDecimal senderBalance = BigDecimal.valueOf(1_000_000L); + + private Repository repository; + private AccountRepository accountRepository; + private BlockData genesisBlockData; + private PrivateKeyAccount sender; + private PrivateKeyAccount generator; + private byte[] reference; + + @Before + public void createTestAccounts() throws DataException { RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl); RepositoryManager.setRepositoryFactory(repositoryFactory); - } - @AfterClass - public static void closeRepository() throws DataException { - RepositoryManager.closeRepositoryFactory(); - } - - @Test - public void testPaymentTransactions() throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight()); } @@ -59,70 +69,127 @@ public class TransactionTests { // This needs to be called outside of acquiring our own repository or it will deadlock BlockChain.validate(); - try (final Repository repository = RepositoryManager.getRepository()) { - // Grab genesis block - BlockData genesisBlockData = repository.getBlockRepository().fromHeight(1); + // Grab repository for further use, including during test itself + repository = RepositoryManager.getRepository(); - AccountRepository accountRepository = repository.getAccountRepository(); + // Grab genesis block + genesisBlockData = repository.getBlockRepository().fromHeight(1); - // Create test generator account - BigDecimal generatorBalance = BigDecimal.valueOf(1_000_000_000L); - PrivateKeyAccount generator = new PrivateKeyAccount(repository, generatorSeed); - accountRepository.save(new AccountData(generator.getAddress(), generatorSeed)); - accountRepository.save(new AccountBalanceData(generator.getAddress(), Asset.QORA, generatorBalance)); + accountRepository = repository.getAccountRepository(); - // Create test sender account - PrivateKeyAccount sender = new PrivateKeyAccount(repository, senderSeed); + // Create test generator account + generator = new PrivateKeyAccount(repository, generatorSeed); + accountRepository.save(new AccountData(generator.getAddress(), generatorSeed)); + accountRepository.save(new AccountBalanceData(generator.getAddress(), Asset.QORA, generatorBalance)); - // Mock account - byte[] reference = senderSeed; - accountRepository.save(new AccountData(sender.getAddress(), reference)); + // Create test sender account + sender = new PrivateKeyAccount(repository, senderSeed); - // Mock balance - BigDecimal initialBalance = BigDecimal.valueOf(1_000_000L); - accountRepository.save(new AccountBalanceData(sender.getAddress(), Asset.QORA, initialBalance)); + // Mock account + reference = senderSeed; + accountRepository.save(new AccountData(sender.getAddress(), reference)); - repository.saveChanges(); + // Mock balance + accountRepository.save(new AccountBalanceData(sender.getAddress(), Asset.QORA, senderBalance)); - // Make a new payment transaction - Account recipient = new PublicKeyAccount(repository, recipientSeed); - BigDecimal amount = BigDecimal.valueOf(1_000L); - BigDecimal fee = BigDecimal.ONE; - long timestamp = genesisBlockData.getTimestamp() + 1_000; - PaymentTransactionData paymentTransactionData = new PaymentTransactionData(sender.getPublicKey(), recipient.getAddress(), amount, fee, timestamp, - reference); + repository.saveChanges(); + } - Transaction paymentTransaction = new PaymentTransaction(repository, paymentTransactionData); - paymentTransaction.calcSignature(sender); - assertTrue(paymentTransaction.isSignatureValid()); - assertEquals(ValidationResult.OK, paymentTransaction.isValid()); + @After + public void closeRepository() throws DataException { + RepositoryManager.closeRepositoryFactory(); + } - // Forge new block with payment transaction - Block block = new Block(repository, genesisBlockData, generator, null, null); - block.addTransaction(paymentTransactionData); - block.sign(); + @Test + public void testPaymentTransaction() throws DataException { + // Make a new payment transaction + Account recipient = new PublicKeyAccount(repository, recipientSeed); + BigDecimal amount = BigDecimal.valueOf(1_000L); + BigDecimal fee = BigDecimal.ONE; + long timestamp = genesisBlockData.getTimestamp() + 1_000; + PaymentTransactionData paymentTransactionData = new PaymentTransactionData(sender.getPublicKey(), recipient.getAddress(), amount, fee, timestamp, + reference); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + Transaction paymentTransaction = new PaymentTransaction(repository, paymentTransactionData); + paymentTransaction.calcSignature(sender); + assertTrue(paymentTransaction.isSignatureValid()); + assertEquals(ValidationResult.OK, paymentTransaction.isValid()); - block.process(); - repository.saveChanges(); + // Forge new block with payment transaction + Block block = new Block(repository, genesisBlockData, generator, null, null); + block.addTransaction(paymentTransactionData); + block.sign(); - // Check sender's balance - BigDecimal expectedBalance = initialBalance.subtract(amount).subtract(fee); - BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); - // Fee should be in generator's balance - expectedBalance = generatorBalance.add(fee); - actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + block.process(); + repository.saveChanges(); - // Amount should be in recipient's balance - expectedBalance = amount; - actualBalance = accountRepository.getBalance(recipient.getAddress(), Asset.QORA).getBalance(); - assertTrue("Recipient's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); - } + // Check sender's balance + BigDecimal expectedBalance = senderBalance.subtract(amount).subtract(fee); + BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); + assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + + // Fee should be in generator's balance + expectedBalance = generatorBalance.add(fee); + actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); + assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + + // Amount should be in recipient's balance + expectedBalance = amount; + actualBalance = accountRepository.getBalance(recipient.getAddress(), Asset.QORA).getBalance(); + assertTrue("Recipient's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + } + + @Test + public void testCreatePollTransaction() throws DataException { + // XXX This test fails unless GenesisBlock's timestamp is set to something after BlockChain.VOTING_RELEASE_TIMESTAMP so we need better testing setup + + // Make a new create poll transaction + String pollName = "test poll"; + String description = "test poll description"; + + List pollOptions = new ArrayList(); + pollOptions.add(new PollOptionData("abort")); + pollOptions.add(new PollOptionData("retry")); + pollOptions.add(new PollOptionData("fail")); + + Account recipient = new PublicKeyAccount(repository, recipientSeed); + BigDecimal fee = BigDecimal.ONE; + long timestamp = genesisBlockData.getTimestamp() + 1_000; + CreatePollTransactionData createPollTransactionData = new CreatePollTransactionData(sender.getPublicKey(), recipient.getAddress(), pollName, + description, pollOptions, fee, timestamp, reference); + + Transaction createPollTransaction = new CreatePollTransaction(repository, createPollTransactionData); + createPollTransaction.calcSignature(sender); + assertTrue(createPollTransaction.isSignatureValid()); + assertEquals(ValidationResult.OK, createPollTransaction.isValid()); + + // Forge new block with payment transaction + Block block = new Block(repository, genesisBlockData, generator, null, null); + block.addTransaction(createPollTransactionData); + block.sign(); + + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + + block.process(); + repository.saveChanges(); + + // Check sender's balance + BigDecimal expectedBalance = senderBalance.subtract(fee); + BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); + assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + + // Fee should be in generator's balance + expectedBalance = generatorBalance.add(fee); + actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); + assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + + // Check poll was created + PollData actualPollData = this.repository.getVotingRepository().fromPollName(pollName); + assertNotNull(actualPollData); } } \ No newline at end of file diff --git a/src/transform/transaction/CreatePollTransactionTransformer.java b/src/transform/transaction/CreatePollTransactionTransformer.java index fe30cbfa..03467fe6 100644 --- a/src/transform/transaction/CreatePollTransactionTransformer.java +++ b/src/transform/transaction/CreatePollTransactionTransformer.java @@ -111,7 +111,9 @@ public class CreatePollTransactionTransformer extends TransactionTransformer { } Serialization.serializeBigDecimal(bytes, createPollTransactionData.getFee()); - bytes.write(createPollTransactionData.getSignature()); + + if (createPollTransactionData.getSignature() != null) + bytes.write(createPollTransactionData.getSignature()); return bytes.toByteArray(); } catch (IOException | ClassCastException e) {