Work on Polls

Added CreatePollTransactionData constructor that doesn't need signature (for creating new transactions).

Added missing "published" timestamp support to PollData and Poll.

Removed extraneous timestamp test from Block.isValid.

Corrected inconsistent column names in poll-related tables in HSQLDBDatabaseUpdates.

HSQLDBRepository.checkedExecute(PreparedStatement) now takes extra Object... params
so that values can be bound to placeholders. Code moved from checkedExecute(String, Object...).
This fixes an issue with exists(String, String, Object...) where placeholders weren't having
any values bound to them. (Also removed "ORDER BY NULL" clause which isn't supported by HSQLDB).

HSQLDBVotingRepository method stubs fleshed out with real code.

TransactionTests rejigged but more work needed to test various transactions before/after their feature
release. e.g. testing create poll transactions before & after they supposedly went live.

Could do with a GenesisBlock constructor that takes a timestamp for testing purposes?

CreatePollTransactionTransformer now skips serializing a null signature,
in the same way PaymentTransactionTransformer does, to aid getBytesLessSignature().
This will probably need to be rolled out to all other transaction types.
This commit is contained in:
catbref 2018-06-21 12:38:45 +01:00
parent 795da06505
commit 05e0fd92b9
10 changed files with 267 additions and 88 deletions

View File

@ -28,6 +28,11 @@ public class CreatePollTransactionData extends TransactionData {
this.pollOptions = pollOptions; this.pollOptions = pollOptions;
} }
public CreatePollTransactionData(byte[] creatorPublicKey, String owner, String pollName, String description, List<PollOptionData> pollOptions,
BigDecimal fee, long timestamp, byte[] reference) {
this(creatorPublicKey, owner, pollName, description, pollOptions, fee, timestamp, reference, null);
}
// Getters/setters // Getters/setters
public byte[] getCreatorPublicKey() { public byte[] getCreatorPublicKey() {

View File

@ -12,14 +12,17 @@ public class PollData {
private String pollName; private String pollName;
private String description; private String description;
private List<PollOptionData> pollOptions; private List<PollOptionData> pollOptions;
private long published;
// Constructors // Constructors
public PollData(byte[] creatorPublicKey, String owner, String pollName, String description, List<PollOptionData> pollOptions) { public PollData(byte[] creatorPublicKey, String owner, String pollName, String description, List<PollOptionData> pollOptions, long published) {
this.creatorPublicKey = creatorPublicKey; this.creatorPublicKey = creatorPublicKey;
this.owner = owner;
this.pollName = pollName; this.pollName = pollName;
this.description = description; this.description = description;
this.pollOptions = pollOptions; this.pollOptions = pollOptions;
this.published = published;
} }
// Getters/setters // Getters/setters
@ -44,4 +47,12 @@ public class PollData {
return this.pollOptions; return this.pollOptions;
} }
public long getPublished() {
return this.published;
}
public void setPublished(long published) {
this.published = published;
}
} }

View File

@ -483,8 +483,7 @@ public class Block {
Block parentBlock = new Block(this.repository, parentBlockData); Block parentBlock = new Block(this.repository, parentBlockData);
// Check timestamp is newer than parent timestamp // Check timestamp is newer than parent timestamp
if (this.blockData.getTimestamp() <= parentBlockData.getTimestamp() if (this.blockData.getTimestamp() <= parentBlockData.getTimestamp())
|| this.blockData.getTimestamp() - BlockChain.BLOCK_TIMESTAMP_MARGIN > NTP.getTime())
return ValidationResult.TIMESTAMP_OLDER_THAN_PARENT; return ValidationResult.TIMESTAMP_OLDER_THAN_PARENT;
// Check timestamp is not in the future (within configurable ~500ms margin) // Check timestamp is not in the future (within configurable ~500ms margin)

View File

@ -17,6 +17,7 @@ import qora.crypto.Crypto;
import qora.transaction.Transaction; import qora.transaction.Transaction;
import repository.DataException; import repository.DataException;
import repository.Repository; import repository.Repository;
import utils.NTP;
public class GenesisBlock extends Block { public class GenesisBlock extends Block {

View File

@ -27,7 +27,7 @@ public class Poll {
public Poll(Repository repository, CreatePollTransactionData createPollTransactionData) { public Poll(Repository repository, CreatePollTransactionData createPollTransactionData) {
this.repository = repository; this.repository = repository;
this.pollData = new PollData(createPollTransactionData.getCreatorPublicKey(), createPollTransactionData.getOwner(), 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 { public Poll(Repository repository, String pollName) throws DataException {

View File

@ -24,6 +24,7 @@ public class HSQLDBDatabaseUpdates {
*/ */
private static void incrementDatabaseVersion(Connection connection) throws SQLException { private static void incrementDatabaseVersion(Connection connection) throws SQLException {
connection.createStatement().execute("UPDATE DatabaseInfo SET version = version + 1"); 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, " + "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, " + "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)"); + "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)"); 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)"); 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)"); 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"); stmt.execute("SET TABLE Blocks NEW SPACE");
break; break;
@ -111,26 +116,33 @@ public class HSQLDBDatabaseUpdates {
// Generic transactions (null reference, creator and milestone_block for genesis transactions) // 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, " 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)"); + "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)"); stmt.execute("CREATE INDEX TransactionTypeIndex ON Transactions (type)");
// For finding transactions using timestamp.
stmt.execute("CREATE INDEX TransactionCreationIndex ON Transactions (creation)"); 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)"); 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)"); 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"); stmt.execute("SET TABLE Transactions NEW SPACE");
// Transaction-Block mapping ("signature" is unique as a transaction cannot be included in more than one block) // 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, " 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, " + "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)"); + "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"); stmt.execute("SET TABLE BlockTransactions NEW SPACE");
// Unconfirmed transactions // 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 TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, expiry TIMESTAMP NOT NULL)");
stmt.execute("CREATE INDEX UnconfirmedTransactionExpiryIndex ON UnconfirmedTransactions (expiry)"); stmt.execute("CREATE INDEX UnconfirmedTransactionExpiryIndex ON UnconfirmedTransactions (expiry)");
// Transaction recipients // Transaction recipients
stmt.execute("CREATE TABLE TransactionRecipients (signature Signature, recipient QoraAddress NOT NULL, " stmt.execute("CREATE TABLE TransactionRecipients (signature Signature, recipient QoraAddress NOT NULL, "
+ "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + "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"); stmt.execute("SET TABLE TransactionRecipients NEW SPACE");
break; break;
@ -184,11 +196,11 @@ public class HSQLDBDatabaseUpdates {
case 10: case 10:
// Create Poll Transactions // Create Poll Transactions
stmt.execute("CREATE TABLE CreatePollTransactions (signature Signature, creator QoraPublicKey NOT NULL, owner QoraAddress 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, " + "poll_name PollName NOT NULL, description VARCHAR(4000) NOT NULL, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + "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 // 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, " stmt.execute("CREATE TABLE CreatePollTransactionOptions (signature Signature, option_name PollOption, "
+ "PRIMARY KEY (signature, option), FOREIGN KEY (signature) REFERENCES CreatePollTransactions (signature) ON DELETE CASCADE)"); + "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 // For the future: add flag to polls to allow one or multiple votes per voter
break; break;
@ -272,6 +284,7 @@ public class HSQLDBDatabaseUpdates {
stmt.execute( stmt.execute(
"CREATE TABLE Assets (asset_id AssetID IDENTITY, owner QoraAddress 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)"); + "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)"); stmt.execute("CREATE INDEX AssetNameIndex on Assets (asset_name)");
break; 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, " "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, " + "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))"); + "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 AssetOrderHaveIndex on AssetOrders (have_asset_id, is_closed)");
stmt.execute("CREATE INDEX AssetOrderWantIndex on AssetOrders (want_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; break;
default: default:

View File

@ -112,15 +112,7 @@ public class HSQLDBRepository implements Repository {
public ResultSet checkedExecute(String sql, Object... objects) throws SQLException { public ResultSet checkedExecute(String sql, Object... objects) throws SQLException {
PreparedStatement preparedStatement = this.connection.prepareStatement(sql); PreparedStatement preparedStatement = this.connection.prepareStatement(sql);
for (int i = 0; i < objects.length; ++i) return this.checkedExecute(preparedStatement, objects);
// 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);
} }
/** /**
@ -129,10 +121,19 @@ public class HSQLDBRepository implements Repository {
* <b>Note: calls ResultSet.next()</b> therefore returned ResultSet is already pointing to first row. * <b>Note: calls ResultSet.next()</b> therefore returned ResultSet is already pointing to first row.
* *
* @param preparedStatement * @param preparedStatement
* @param objects
* @return ResultSet, or null if there are no found rows * @return ResultSet, or null if there are no found rows
* @throws SQLException * @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()) if (!preparedStatement.execute())
throw new SQLException("Fetching from database produced no results"); throw new SQLException("Fetching from database produced no results");
@ -183,9 +184,8 @@ public class HSQLDBRepository implements Repository {
* @throws SQLException * @throws SQLException
*/ */
public boolean exists(String tableName, String whereClause, Object... objects) throws SQLException { public boolean exists(String tableName, String whereClause, Object... objects) throws SQLException {
PreparedStatement preparedStatement = this.connection PreparedStatement preparedStatement = this.connection.prepareStatement("SELECT TRUE FROM " + tableName + " WHERE " + whereClause + " LIMIT 1");
.prepareStatement("SELECT TRUE FROM " + tableName + " WHERE " + whereClause + " ORDER BY NULL LIMIT 1"); ResultSet resultSet = this.checkedExecute(preparedStatement, objects);
ResultSet resultSet = this.checkedExecute(preparedStatement);
if (resultSet == null) if (resultSet == null)
return false; return false;

View File

@ -1,6 +1,13 @@
package repository.hsqldb; 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.PollData;
import data.voting.PollOptionData;
import repository.VotingRepository; import repository.VotingRepository;
import repository.DataException; import repository.DataException;
@ -13,21 +20,77 @@ public class HSQLDBVotingRepository implements VotingRepository {
} }
public PollData fromPollName(String pollName) throws DataException { public PollData fromPollName(String pollName) throws DataException {
// TODO try {
ResultSet resultSet = this.repository.checkedExecute("SELECT description, creator, owner, published FROM Polls WHERE poll_name = ?", pollName);
if (resultSet == null)
return 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<PollOptionData> pollOptions = new ArrayList<PollOptionData>();
// 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 { public boolean pollExists(String pollName) throws DataException {
// TODO try {
return false; 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 { 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 { 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);
}
} }
} }

View File

@ -3,8 +3,12 @@ package test;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
@ -13,13 +17,17 @@ import com.google.common.hash.HashCode;
import data.account.AccountBalanceData; import data.account.AccountBalanceData;
import data.account.AccountData; import data.account.AccountData;
import data.block.BlockData; import data.block.BlockData;
import data.transaction.CreatePollTransactionData;
import data.transaction.PaymentTransactionData; import data.transaction.PaymentTransactionData;
import data.voting.PollData;
import data.voting.PollOptionData;
import qora.account.Account; import qora.account.Account;
import qora.account.PrivateKeyAccount; import qora.account.PrivateKeyAccount;
import qora.account.PublicKeyAccount; import qora.account.PublicKeyAccount;
import qora.assets.Asset; import qora.assets.Asset;
import qora.block.Block; import qora.block.Block;
import qora.block.BlockChain; import qora.block.BlockChain;
import qora.transaction.CreatePollTransaction;
import qora.transaction.PaymentTransaction; import qora.transaction.PaymentTransaction;
import qora.transaction.Transaction; import qora.transaction.Transaction;
import qora.transaction.Transaction.ValidationResult; import qora.transaction.Transaction.ValidationResult;
@ -39,19 +47,21 @@ public class TransactionTests {
private static final byte[] senderSeed = HashCode.fromString("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef").asBytes(); private static final byte[] senderSeed = HashCode.fromString("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef").asBytes();
private static final byte[] recipientSeed = HashCode.fromString("fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210").asBytes(); private static final byte[] recipientSeed = HashCode.fromString("fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210").asBytes();
@BeforeClass private static final BigDecimal generatorBalance = BigDecimal.valueOf(1_000_000_000L);
public static void setRepository() throws DataException { 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); RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory); RepositoryManager.setRepositoryFactory(repositoryFactory);
}
@AfterClass
public static void closeRepository() throws DataException {
RepositoryManager.closeRepositoryFactory();
}
@Test
public void testPaymentTransactions() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight()); assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight());
} }
@ -59,31 +69,39 @@ public class TransactionTests {
// This needs to be called outside of acquiring our own repository or it will deadlock // This needs to be called outside of acquiring our own repository or it will deadlock
BlockChain.validate(); BlockChain.validate();
try (final Repository repository = RepositoryManager.getRepository()) { // Grab repository for further use, including during test itself
// Grab genesis block repository = RepositoryManager.getRepository();
BlockData genesisBlockData = repository.getBlockRepository().fromHeight(1);
AccountRepository accountRepository = repository.getAccountRepository(); // Grab genesis block
genesisBlockData = repository.getBlockRepository().fromHeight(1);
accountRepository = repository.getAccountRepository();
// Create test generator account // Create test generator account
BigDecimal generatorBalance = BigDecimal.valueOf(1_000_000_000L); generator = new PrivateKeyAccount(repository, generatorSeed);
PrivateKeyAccount generator = new PrivateKeyAccount(repository, generatorSeed);
accountRepository.save(new AccountData(generator.getAddress(), generatorSeed)); accountRepository.save(new AccountData(generator.getAddress(), generatorSeed));
accountRepository.save(new AccountBalanceData(generator.getAddress(), Asset.QORA, generatorBalance)); accountRepository.save(new AccountBalanceData(generator.getAddress(), Asset.QORA, generatorBalance));
// Create test sender account // Create test sender account
PrivateKeyAccount sender = new PrivateKeyAccount(repository, senderSeed); sender = new PrivateKeyAccount(repository, senderSeed);
// Mock account // Mock account
byte[] reference = senderSeed; reference = senderSeed;
accountRepository.save(new AccountData(sender.getAddress(), reference)); accountRepository.save(new AccountData(sender.getAddress(), reference));
// Mock balance // Mock balance
BigDecimal initialBalance = BigDecimal.valueOf(1_000_000L); accountRepository.save(new AccountBalanceData(sender.getAddress(), Asset.QORA, senderBalance));
accountRepository.save(new AccountBalanceData(sender.getAddress(), Asset.QORA, initialBalance));
repository.saveChanges(); repository.saveChanges();
}
@After
public void closeRepository() throws DataException {
RepositoryManager.closeRepositoryFactory();
}
@Test
public void testPaymentTransaction() throws DataException {
// Make a new payment transaction // Make a new payment transaction
Account recipient = new PublicKeyAccount(repository, recipientSeed); Account recipient = new PublicKeyAccount(repository, recipientSeed);
BigDecimal amount = BigDecimal.valueOf(1_000L); BigDecimal amount = BigDecimal.valueOf(1_000L);
@ -109,7 +127,7 @@ public class TransactionTests {
repository.saveChanges(); repository.saveChanges();
// Check sender's balance // Check sender's balance
BigDecimal expectedBalance = initialBalance.subtract(amount).subtract(fee); BigDecimal expectedBalance = senderBalance.subtract(amount).subtract(fee);
BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance();
assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
@ -123,6 +141,55 @@ public class TransactionTests {
actualBalance = accountRepository.getBalance(recipient.getAddress(), Asset.QORA).getBalance(); actualBalance = accountRepository.getBalance(recipient.getAddress(), Asset.QORA).getBalance();
assertTrue("Recipient's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); 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<PollOptionData> pollOptions = new ArrayList<PollOptionData>();
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);
} }
} }

View File

@ -111,6 +111,8 @@ public class CreatePollTransactionTransformer extends TransactionTransformer {
} }
Serialization.serializeBigDecimal(bytes, createPollTransactionData.getFee()); Serialization.serializeBigDecimal(bytes, createPollTransactionData.getFee());
if (createPollTransactionData.getSignature() != null)
bytes.write(createPollTransactionData.getSignature()); bytes.write(createPollTransactionData.getSignature());
return bytes.toByteArray(); return bytes.toByteArray();