diff --git a/src/data/transaction/Transaction.java b/src/data/transaction/Transaction.java index 443456fd..6b0f835c 100644 --- a/src/data/transaction/Transaction.java +++ b/src/data/transaction/Transaction.java @@ -2,11 +2,12 @@ package data.transaction; import java.math.BigDecimal; import java.util.Map; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toMap; import data.account.PublicKeyAccount; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toMap; + public abstract class Transaction { // Transaction types diff --git a/src/qora/account/Account.java b/src/qora/account/Account.java index b65ff629..6e01c7f2 100644 --- a/src/qora/account/Account.java +++ b/src/qora/account/Account.java @@ -5,7 +5,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import database.DB; -import database.SaveHelper; +import repository.hsqldb.HSQLDBSaver; public class Account { @@ -48,7 +48,7 @@ public class Account { } public void setConfirmedBalance(long assetId, BigDecimal balance) throws SQLException { - SaveHelper saveHelper = new SaveHelper("AccountBalances"); + HSQLDBSaver saveHelper = new HSQLDBSaver("AccountBalances"); saveHelper.bind("account", this.getAddress()).bind("asset_id", assetId).bind("balance", balance); saveHelper.execute(); } @@ -81,7 +81,7 @@ public class Account { * @throws SQLException */ public void setLastReference(byte[] reference) throws SQLException { - SaveHelper saveHelper = new SaveHelper("Accounts"); + HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts"); saveHelper.bind("account", this.getAddress()).bind("reference", reference); saveHelper.execute(); } diff --git a/src/qora/assets/Asset.java b/src/qora/assets/Asset.java index 30341807..efe207ee 100644 --- a/src/qora/assets/Asset.java +++ b/src/qora/assets/Asset.java @@ -5,9 +5,9 @@ import java.sql.SQLException; import database.DB; import database.NoDataFoundException; -import database.SaveHelper; import qora.account.Account; -import qora.transaction.Transaction; +import qora.transaction.TransactionHandler; +import repository.hsqldb.HSQLDBSaver; /* * TODO: @@ -91,7 +91,7 @@ public class Asset { this.description = rs.getString(3); this.quantity = rs.getLong(4); this.isDivisible = rs.getBoolean(5); - this.reference = DB.getResultSetBytes(rs.getBinaryStream(6), Transaction.REFERENCE_LENGTH); + this.reference = DB.getResultSetBytes(rs.getBinaryStream(6), TransactionHandler.REFERENCE_LENGTH); } public static Asset fromAssetId(long assetId) throws SQLException { @@ -103,7 +103,7 @@ public class Asset { } public void save() throws SQLException { - SaveHelper saveHelper = new SaveHelper("Assets"); + HSQLDBSaver saveHelper = new HSQLDBSaver("Assets"); saveHelper.bind("asset_id", this.assetId).bind("owner", this.owner.getAddress()).bind("asset_name", this.name).bind("description", this.description) .bind("quantity", this.quantity).bind("is_divisible", this.isDivisible).bind("reference", this.reference); saveHelper.execute(); diff --git a/src/qora/assets/Order.java b/src/qora/assets/Order.java index 24018467..ae0a4f22 100644 --- a/src/qora/assets/Order.java +++ b/src/qora/assets/Order.java @@ -5,7 +5,7 @@ import java.math.BigInteger; import java.util.List; import qora.account.Account; -import utils.ParseException; +import transform.TransformationException; public class Order implements Comparable { @@ -109,7 +109,7 @@ public class Order implements Comparable { // Converters - public static Order parse(byte[] data) throws ParseException { + public static Order parse(byte[] data) throws TransformationException { // TODO return null; } @@ -133,7 +133,7 @@ public class Order implements Comparable { public Order copy() { try { return parse(this.toBytes()); - } catch (ParseException e) { + } catch (TransformationException e) { return null; } } diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index 508b7180..c66220c3 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -24,7 +24,6 @@ import com.google.common.primitives.Longs; import database.DB; import database.NoDataFoundException; -import database.SaveHelper; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; import qora.assets.Asset; @@ -32,11 +31,12 @@ import qora.assets.Order; import qora.assets.Trade; import qora.transaction.CreateOrderTransaction; import qora.transaction.GenesisTransaction; -import qora.transaction.Transaction; -import qora.transaction.TransactionFactory; +import qora.transaction.TransactionHandler; +import repository.hsqldb.HSQLDBSaver; +import qora.transaction.TransactionHandler; +import transform.TransformationException; import utils.Base58; import utils.NTP; -import utils.ParseException; import utils.Serialization; /* @@ -84,7 +84,7 @@ public class Block { protected BigDecimal atFees; // Other properties - protected List transactions; + protected List transactions; protected BigDecimal cachedNextGeneratingBalance; // Property lengths for serialisation @@ -136,7 +136,7 @@ public class Block { this.height = 0; this.transactionCount = 0; - this.transactions = new ArrayList(); + this.transactions = new ArrayList(); this.transactionsSignature = null; this.totalFees = BigDecimal.ZERO.setScale(8); @@ -148,7 +148,7 @@ public class Block { // For instantiating a block that was previously serialized protected Block(int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PublicKeyAccount generator, byte[] generatorSignature, - byte[] transactionsSignature, byte[] atBytes, BigDecimal atFees, List transactions) { + byte[] transactionsSignature, byte[] atBytes, BigDecimal atFees, List transactions) { this(version, reference, timestamp, generatingBalance, generator, atBytes, atFees); this.generatorSignature = generatorSignature; @@ -158,7 +158,7 @@ public class Block { this.transactions = transactions; // Add transactions' fees to totalFees - for (Transaction transaction : this.transactions) + for (TransactionHandler transaction : this.transactions) this.totalFees = this.totalFees.add(transaction.getFee()); } @@ -236,7 +236,7 @@ public class Block { if (this.transactions == null || this.transactions.isEmpty()) return blockLength; - for (Transaction transaction : this.transactions) + for (TransactionHandler transaction : this.transactions) blockLength += TRANSACTION_SIZE_LENGTH + transaction.getDataLength(); return blockLength; @@ -321,13 +321,13 @@ public class Block { * @return * @throws SQLException */ - public List getTransactions() throws SQLException { + public List getTransactions() throws SQLException { // Already loaded? if (this.transactions != null) return this.transactions; // Allocate cache for results - this.transactions = new ArrayList(); + this.transactions = new ArrayList(); ResultSet rs = DB.checkedExecute("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ?", this.getSignature()); if (rs == null) @@ -335,7 +335,7 @@ public class Block { // NB: do-while loop because DB.checkedExecute() implicitly calls ResultSet.next() for us do { - byte[] transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), Transaction.SIGNATURE_LENGTH); + byte[] transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), TransactionHandler.SIGNATURE_LENGTH); this.transactions.add(TransactionFactory.fromSignature(transactionSignature)); // No need to update totalFees as this will be loaded via the Blocks table @@ -407,7 +407,7 @@ public class Block { } protected void save() throws SQLException { - SaveHelper saveHelper = new SaveHelper("Blocks"); + HSQLDBSaver saveHelper = new HSQLDBSaver("Blocks"); saveHelper.bind("signature", this.getSignature()).bind("version", this.version).bind("reference", this.reference) .bind("transaction_count", this.transactionCount).bind("total_fees", this.totalFees).bind("transactions_signature", this.transactionsSignature) @@ -479,11 +479,11 @@ public class Block { JSONArray transactionsJson = new JSONArray(); boolean tradesHappened = false; - for (Transaction transaction : this.getTransactions()) { + for (TransactionHandler transaction : this.getTransactions()) { transactionsJson.add(transaction.toJSON()); // If this is an asset CreateOrderTransaction then check to see if any trades happened - if (transaction.getType() == Transaction.TransactionType.CREATE_ASSET_ORDER) { + if (transaction.getType() == Transaction.TransactionHandler.CREATE_ASSET_ORDER) { CreateOrderTransaction orderTransaction = (CreateOrderTransaction) transaction; Order order = orderTransaction.getOrder(); List trades = order.getTrades(); @@ -541,7 +541,7 @@ public class Block { // Transactions bytes.write(Ints.toByteArray(this.transactionCount)); - for (Transaction transaction : this.getTransactions()) { + for (TransactionHandler transaction : this.getTransactions()) { bytes.write(Ints.toByteArray(transaction.getDataLength())); bytes.write(transaction.toBytes()); } @@ -552,19 +552,19 @@ public class Block { } } - public static Block parse(byte[] data) throws ParseException { + public static Block parse(byte[] data) throws TransformationException { if (data == null) return null; if (data.length < BASE_LENGTH) - throw new ParseException("Byte data too short for Block"); + throw new TransformationException("Byte data too short for Block"); ByteBuffer byteBuffer = ByteBuffer.wrap(data); int version = byteBuffer.getInt(); if (version >= 2 && data.length < BASE_LENGTH + AT_LENGTH) - throw new ParseException("Byte data too short for V2+ Block"); + throw new TransformationException("Byte data too short for V2+ Block"); long timestamp = byteBuffer.getLong(); @@ -585,7 +585,7 @@ public class Block { int atBytesLength = byteBuffer.getInt(); if (atBytesLength > MAX_BLOCK_BYTES) - throw new ParseException("Byte data too long for Block's AT info"); + throw new TransformationException("Byte data too long for Block's AT info"); atBytes = new byte[atBytesLength]; byteBuffer.get(atBytes); @@ -596,26 +596,26 @@ public class Block { int transactionCount = byteBuffer.getInt(); // Parse transactions now, compared to deferred parsing in Gen1, so we can throw ParseException if need be - List transactions = new ArrayList(); + List transactions = new ArrayList(); for (int t = 0; t < transactionCount; ++t) { if (byteBuffer.remaining() < TRANSACTION_SIZE_LENGTH) - throw new ParseException("Byte data too short for Block Transaction length"); + throw new TransformationException("Byte data too short for Block Transaction length"); int transactionLength = byteBuffer.getInt(); if (byteBuffer.remaining() < transactionLength) - throw new ParseException("Byte data too short for Block Transaction"); + throw new TransformationException("Byte data too short for Block Transaction"); if (transactionLength > MAX_BLOCK_BYTES) - throw new ParseException("Byte data too long for Block Transaction"); + throw new TransformationException("Byte data too long for Block Transaction"); byte[] transactionBytes = new byte[transactionLength]; byteBuffer.get(transactionBytes); - Transaction transaction = Transaction.parse(transactionBytes); + TransactionHandler transaction = TransactionHandler.parse(transactionBytes); transactions.add(transaction); } if (byteBuffer.hasRemaining()) - throw new ParseException("Excess byte data found after parsing Block"); + throw new TransformationException("Excess byte data found after parsing Block"); return new Block(version, reference, timestamp, generatingBalance, generator, generatorSignature, transactionsSignature, atBytes, atFees, transactions); } @@ -634,7 +634,7 @@ public class Block { * @throws IllegalStateException * if block's {@code generator} is not a {@code PrivateKeyAccount}. */ - public boolean addTransaction(Transaction transaction) { + public boolean addTransaction(TransactionHandler transaction) { // Can't add to transactions if we haven't loaded existing ones yet if (this.transactions == null) throw new IllegalStateException("Attempted to add transaction to partially loaded database Block"); @@ -710,12 +710,12 @@ public class Block { } private byte[] getBytesForTransactionsSignature() { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + this.transactionCount * Transaction.SIGNATURE_LENGTH); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + this.transactionCount * TransactionHandler.SIGNATURE_LENGTH); try { bytes.write(this.generatorSignature); - for (Transaction transaction : this.getTransactions()) { + for (TransactionHandler transaction : this.getTransactions()) { if (!transaction.isSignatureValid()) return null; @@ -796,7 +796,7 @@ public class Block { // Check transactions Savepoint savepoint = DB.createSavepoint("BLOCK_TRANSACTIONS"); try { - for (Transaction transaction : this.getTransactions()) { + for (TransactionHandler transaction : this.getTransactions()) { // GenesisTransactions are not allowed (GenesisBlock overrides isValid() to allow them) if (transaction instanceof GenesisTransaction) return false; @@ -807,7 +807,7 @@ public class Block { // Check transaction is even valid // NOTE: in Gen1 there was an extra block height passed to DeployATTransaction.isValid - if (transaction.isValid() != Transaction.ValidationResult.OK) + if (transaction.isValid() != TransactionHandler.ValidationResult.OK) return false; // Process transaction to make sure other transactions validate properly @@ -836,8 +836,8 @@ public class Block { public void process() throws SQLException { // Process transactions (we'll link them to this block after saving the block itself) - List transactions = this.getTransactions(); - for (Transaction transaction : transactions) + List transactions = this.getTransactions(); + for (TransactionHandler transaction : transactions) transaction.process(); // If fees are non-zero then add fees to generator's balance @@ -856,7 +856,7 @@ public class Block { // Link transactions to this block, thus removing them from unconfirmed transactions list. for (int sequence = 0; sequence < transactions.size(); ++sequence) { - Transaction transaction = transactions.get(sequence); + TransactionHandler transaction = transactions.get(sequence); // Link transaction to this block BlockTransaction blockTransaction = new BlockTransaction(this.getSignature(), sequence, transaction.getSignature()); diff --git a/src/qora/block/BlockTransaction.java b/src/qora/block/BlockTransaction.java index 38d7d28d..cdf76253 100644 --- a/src/qora/block/BlockTransaction.java +++ b/src/qora/block/BlockTransaction.java @@ -5,9 +5,9 @@ import java.sql.SQLException; import database.DB; import database.NoDataFoundException; -import database.SaveHelper; -import qora.transaction.Transaction; -import qora.transaction.TransactionFactory; +import qora.transaction.TransactionHandler; +import repository.hsqldb.HSQLDBSaver; +import qora.transaction.TransactionHandler; public class BlockTransaction { @@ -50,7 +50,7 @@ public class BlockTransaction { this.blockSignature = blockSignature; this.sequence = sequence; - this.transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), Transaction.SIGNATURE_LENGTH); + this.transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), TransactionHandler.SIGNATURE_LENGTH); } protected BlockTransaction(byte[] transactionSignature) throws SQLException { @@ -95,7 +95,7 @@ public class BlockTransaction { } protected void save() throws SQLException { - SaveHelper saveHelper = new SaveHelper("BlockTransactions"); + HSQLDBSaver saveHelper = new HSQLDBSaver("BlockTransactions"); saveHelper.bind("block_signature", this.blockSignature).bind("sequence", this.sequence).bind("transaction_signature", this.transactionSignature); saveHelper.execute(); } @@ -118,7 +118,7 @@ public class BlockTransaction { * @return Transaction, or null if not found (which should never happen) * @throws SQLException */ - public Transaction getTransaction() throws SQLException { + public TransactionHandler getTransaction() throws SQLException { return TransactionFactory.fromSignature(this.transactionSignature); } diff --git a/src/qora/block/GenesisBlock.java b/src/qora/block/GenesisBlock.java index 9d512093..c79c31e2 100644 --- a/src/qora/block/GenesisBlock.java +++ b/src/qora/block/GenesisBlock.java @@ -14,7 +14,7 @@ import com.google.common.primitives.Longs; import qora.account.GenesisAccount; import qora.crypto.Crypto; import qora.transaction.GenesisTransaction; -import qora.transaction.Transaction; +import qora.transaction.TransactionHandler; public class GenesisBlock extends Block { @@ -31,7 +31,7 @@ public class GenesisBlock extends Block { // Constructors protected GenesisBlock() { super(GENESIS_BLOCK_VERSION, GENESIS_REFERENCE, GENESIS_TIMESTAMP, GENESIS_GENERATING_BALANCE, GENESIS_GENERATOR, GENESIS_GENERATOR_SIGNATURE, - GENESIS_TRANSACTIONS_SIGNATURE, null, null, new ArrayList()); + GENESIS_TRANSACTIONS_SIGNATURE, null, null, new ArrayList()); this.height = 1; @@ -233,7 +233,7 @@ public class GenesisBlock extends Block { // Processing @Override - public boolean addTransaction(Transaction transaction) { + public boolean addTransaction(TransactionHandler transaction) { // The genesis block has a fixed set of transactions so always refuse. return false; } @@ -334,8 +334,8 @@ public class GenesisBlock extends Block { return false; // Validate transactions - for (Transaction transaction : this.getTransactions()) - if (transaction.isValid() != Transaction.ValidationResult.OK) + for (TransactionHandler transaction : this.getTransactions()) + if (transaction.isValid() != TransactionHandler.ValidationResult.OK) return false; return true; diff --git a/src/qora/transaction/CreateOrderTransaction.java b/src/qora/transaction/CreateOrderTransaction.java index 82e3c2a7..fd6c61f6 100644 --- a/src/qora/transaction/CreateOrderTransaction.java +++ b/src/qora/transaction/CreateOrderTransaction.java @@ -15,12 +15,12 @@ import com.google.common.primitives.Longs; import database.DB; import database.NoDataFoundException; -import database.SaveHelper; import qora.account.PublicKeyAccount; import qora.assets.Order; -import utils.ParseException; +import repository.hsqldb.HSQLDBSaver; +import transform.TransformationException; -public class CreateOrderTransaction extends Transaction { +public class CreateOrderTransaction extends TransactionHandler { // Properties private Order order; @@ -100,7 +100,7 @@ public class CreateOrderTransaction extends Transaction { public void save() throws SQLException { super.save(); - SaveHelper saveHelper = new SaveHelper("CreateAssetOrderTransactions"); + HSQLDBSaver saveHelper = new HSQLDBSaver("CreateAssetOrderTransactions"); saveHelper.bind("signature", this.signature).bind("creator", this.creator.getPublicKey()).bind("have_asset_id", this.order.getHaveAssetId()) .bind("amount", this.order.getAmount()).bind("want_asset_id", this.order.getWantAssetId()).bind("price", this.order.getPrice()); saveHelper.execute(); @@ -108,9 +108,9 @@ public class CreateOrderTransaction extends Transaction { // Converters - protected static Transaction parse(ByteBuffer byteBuffer) throws ParseException { + protected static TransactionHandler parse(ByteBuffer byteBuffer) throws TransformationException { if (byteBuffer.remaining() < TYPELESS_LENGTH) - throw new ParseException("Byte data too short for CreateOrderTransaction"); + throw new TransformationException("Byte data too short for CreateOrderTransaction"); // TODO return null; diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java index 53b25acc..88bd40c0 100644 --- a/src/qora/transaction/GenesisTransaction.java +++ b/src/qora/transaction/GenesisTransaction.java @@ -16,17 +16,17 @@ import com.google.common.primitives.Longs; import database.DB; import database.NoDataFoundException; -import database.SaveHelper; import qora.account.Account; import qora.account.GenesisAccount; import qora.account.PrivateKeyAccount; import qora.assets.Asset; import qora.crypto.Crypto; +import repository.hsqldb.HSQLDBSaver; +import transform.TransformationException; import utils.Base58; -import utils.ParseException; import utils.Serialization; -public class GenesisTransaction extends Transaction { +public class GenesisTransaction extends TransactionHandler { // Properties private Account recipient; @@ -105,16 +105,16 @@ public class GenesisTransaction extends Transaction { public void save() throws SQLException { super.save(); - SaveHelper saveHelper = new SaveHelper("GenesisTransactions"); + HSQLDBSaver saveHelper = new HSQLDBSaver("GenesisTransactions"); saveHelper.bind("signature", this.signature).bind("recipient", this.recipient.getAddress()).bind("amount", this.amount); saveHelper.execute(); } // Converters - protected static Transaction parse(ByteBuffer byteBuffer) throws ParseException { + protected static TransactionHandler parse(ByteBuffer byteBuffer) throws TransformationException { if (byteBuffer.remaining() < TYPELESS_LENGTH) - throw new ParseException("Byte data too short for GenesisTransaction"); + throw new TransformationException("Byte data too short for GenesisTransaction"); long timestamp = byteBuffer.getLong(); String recipient = Serialization.deserializeRecipient(byteBuffer); diff --git a/src/qora/transaction/IssueAssetTransaction.java b/src/qora/transaction/IssueAssetTransaction.java index 58aa2956..3a5d4b12 100644 --- a/src/qora/transaction/IssueAssetTransaction.java +++ b/src/qora/transaction/IssueAssetTransaction.java @@ -16,18 +16,18 @@ import com.google.common.primitives.Longs; import database.DB; import database.NoDataFoundException; -import database.SaveHelper; import qora.account.Account; import qora.account.PublicKeyAccount; import qora.assets.Asset; import qora.block.Block; import qora.crypto.Crypto; +import repository.hsqldb.HSQLDBSaver; +import transform.TransformationException; import utils.Base58; import utils.NTP; -import utils.ParseException; import utils.Serialization; -public class IssueAssetTransaction extends Transaction { +public class IssueAssetTransaction extends TransactionHandler { // Properties private PublicKeyAccount issuer; @@ -186,7 +186,7 @@ public class IssueAssetTransaction extends Transaction { public void save() throws SQLException { super.save(); - SaveHelper saveHelper = new SaveHelper("IssueAssetTransactions"); + HSQLDBSaver saveHelper = new HSQLDBSaver("IssueAssetTransactions"); saveHelper.bind("signature", this.signature).bind("creator", this.creator.getPublicKey()).bind("asset_name", this.assetName) .bind("description", this.description).bind("quantity", this.quantity).bind("is_divisible", this.isDivisible).bind("asset_id", this.assetId); saveHelper.execute(); @@ -194,9 +194,9 @@ public class IssueAssetTransaction extends Transaction { // Converters - protected static Transaction parse(ByteBuffer byteBuffer) throws ParseException { + protected static TransactionHandler parse(ByteBuffer byteBuffer) throws TransformationException { if (byteBuffer.remaining() < TYPELESS_LENGTH) - throw new ParseException("Byte data too short for IssueAssetTransaction"); + throw new TransformationException("Byte data too short for IssueAssetTransaction"); long timestamp = byteBuffer.getLong(); @@ -211,7 +211,7 @@ public class IssueAssetTransaction extends Transaction { // Still need to make sure there are enough bytes left for remaining fields if (byteBuffer.remaining() < QUANTITY_LENGTH + IS_DIVISIBLE_LENGTH + SIGNATURE_LENGTH) - throw new ParseException("Byte data too short for IssueAssetTransaction"); + throw new TransformationException("Byte data too short for IssueAssetTransaction"); long quantity = byteBuffer.getLong(); boolean isDivisible = byteBuffer.get() != 0; diff --git a/src/qora/transaction/MessageTransaction.java b/src/qora/transaction/MessageTransaction.java index 52cc78d7..c1ece0c1 100644 --- a/src/qora/transaction/MessageTransaction.java +++ b/src/qora/transaction/MessageTransaction.java @@ -17,18 +17,18 @@ import com.google.common.primitives.Longs; import database.DB; import database.NoDataFoundException; -import database.SaveHelper; import qora.account.Account; import qora.account.PublicKeyAccount; import qora.assets.Asset; import qora.block.Block; import qora.block.BlockChain; import qora.crypto.Crypto; +import repository.hsqldb.HSQLDBSaver; +import transform.TransformationException; import utils.Base58; -import utils.ParseException; import utils.Serialization; -public class MessageTransaction extends Transaction { +public class MessageTransaction extends TransactionHandler { // Properties protected int version; @@ -60,7 +60,7 @@ public class MessageTransaction extends Transaction { boolean isEncrypted, long timestamp, byte[] reference, byte[] signature) { super(TransactionType.MESSAGE, fee, sender, timestamp, reference, signature); - this.version = Transaction.getVersionByTimestamp(this.timestamp); + this.version = TransactionHandler.getVersionByTimestamp(this.timestamp); this.sender = sender; this.recipient = new Account(recipient); @@ -165,7 +165,7 @@ public class MessageTransaction extends Transaction { public void save() throws SQLException { super.save(); - SaveHelper saveHelper = new SaveHelper("MessageTransactions"); + HSQLDBSaver saveHelper = new HSQLDBSaver("MessageTransactions"); saveHelper.bind("signature", this.signature).bind("version", this.version).bind("sender", this.sender.getPublicKey()) .bind("recipient", this.recipient.getAddress()).bind("is_text", this.isText).bind("is_encrypted", this.isEncrypted).bind("amount", this.amount) .bind("asset_id", this.assetId).bind("data", this.data); @@ -174,18 +174,18 @@ public class MessageTransaction extends Transaction { // Converters - protected static Transaction parse(ByteBuffer byteBuffer) throws ParseException { + protected static TransactionHandler parse(ByteBuffer byteBuffer) throws TransformationException { if (byteBuffer.remaining() < TIMESTAMP_LENGTH) - throw new ParseException("Byte data too short for MessageTransaction"); + throw new TransformationException("Byte data too short for MessageTransaction"); long timestamp = byteBuffer.getLong(); - int version = Transaction.getVersionByTimestamp(timestamp); + int version = TransactionHandler.getVersionByTimestamp(timestamp); int minimumRemaining = version == 1 ? TYPELESS_DATALESS_LENGTH_V1 : TYPELESS_DATALESS_LENGTH_V3; minimumRemaining -= TIMESTAMP_LENGTH; // Already read above if (byteBuffer.remaining() < minimumRemaining) - throw new ParseException("Byte data too short for MessageTransaction"); + throw new TransformationException("Byte data too short for MessageTransaction"); byte[] reference = new byte[REFERENCE_LENGTH]; byteBuffer.get(reference); @@ -204,7 +204,7 @@ public class MessageTransaction extends Transaction { int dataSize = byteBuffer.getInt(0); // Don't allow invalid dataSize here to avoid run-time issues if (dataSize > MAX_DATA_SIZE) - throw new ParseException("MessageTransaction data size too large"); + throw new TransformationException("MessageTransaction data size too large"); byte[] data = new byte[dataSize]; byteBuffer.get(data); @@ -277,7 +277,7 @@ public class MessageTransaction extends Transaction { // Lowest cost checks first // Are message transactions even allowed at this point? - if (this.version != Transaction.getVersionByTimestamp(this.timestamp)) + if (this.version != TransactionHandler.getVersionByTimestamp(this.timestamp)) return ValidationResult.NOT_YET_RELEASED; if (BlockChain.getHeight() < Block.MESSAGE_RELEASE_HEIGHT) diff --git a/src/qora/transaction/PaymentTransaction.java b/src/qora/transaction/PaymentTransaction.java index be7f06d7..2a51afad 100644 --- a/src/qora/transaction/PaymentTransaction.java +++ b/src/qora/transaction/PaymentTransaction.java @@ -16,16 +16,16 @@ import com.google.common.primitives.Longs; import database.DB; import database.NoDataFoundException; -import database.SaveHelper; import qora.account.Account; import qora.account.PublicKeyAccount; import qora.assets.Asset; import qora.crypto.Crypto; +import repository.hsqldb.HSQLDBSaver; +import transform.TransformationException; import utils.Base58; -import utils.ParseException; import utils.Serialization; -public class PaymentTransaction extends Transaction { +public class PaymentTransaction extends TransactionHandler { // Properties private PublicKeyAccount sender; @@ -113,7 +113,7 @@ public class PaymentTransaction extends Transaction { public void save() throws SQLException { super.save(); - SaveHelper saveHelper = new SaveHelper("PaymentTransactions"); + HSQLDBSaver saveHelper = new HSQLDBSaver("PaymentTransactions"); saveHelper.bind("signature", this.signature).bind("sender", this.sender.getPublicKey()).bind("recipient", this.recipient.getAddress()).bind("amount", this.amount); saveHelper.execute(); @@ -121,9 +121,9 @@ public class PaymentTransaction extends Transaction { // Converters - protected static Transaction parse(ByteBuffer byteBuffer) throws ParseException { + protected static TransactionHandler parse(ByteBuffer byteBuffer) throws TransformationException { if (byteBuffer.remaining() < TYPELESS_LENGTH) - throw new ParseException("Byte data too short for PaymentTransaction"); + throw new TransformationException("Byte data too short for PaymentTransaction"); long timestamp = byteBuffer.getLong(); diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java deleted file mode 100644 index 6ad9e23c..00000000 --- a/src/qora/transaction/Transaction.java +++ /dev/null @@ -1,441 +0,0 @@ -package qora.transaction; - -import java.math.BigDecimal; -import java.math.MathContext; -import java.nio.ByteBuffer; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.util.Arrays; -import java.util.Map; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toMap; - -import org.json.simple.JSONObject; - -import database.DB; -import database.NoDataFoundException; -import database.SaveHelper; -import qora.account.PrivateKeyAccount; -import qora.account.PublicKeyAccount; -import qora.block.Block; -import qora.block.BlockChain; -import qora.block.BlockTransaction; -import repository.Repository; -import repository.RepositoryManager; -import settings.Settings; - -import utils.Base58; -import utils.ParseException; - -public abstract class Transaction { - - // Transaction types - public enum TransactionType { - GENESIS(1), PAYMENT(2), REGISTER_NAME(3), UPDATE_NAME(4), SELL_NAME(5), CANCEL_SELL_NAME(6), BUY_NAME(7), CREATE_POLL(8), VOTE_ON_POLL(9), ARBITRARY( - 10), ISSUE_ASSET(11), TRANSFER_ASSET(12), CREATE_ASSET_ORDER(13), CANCEL_ASSET_ORDER(14), MULTIPAYMENT(15), DEPLOY_AT(16), MESSAGE(17); - - public final int value; - - private final static Map map = stream(TransactionType.values()).collect(toMap(type -> type.value, type -> type)); - - TransactionType(int value) { - this.value = value; - } - - public static TransactionType valueOf(int value) { - return map.get(value); - } - } - - // 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_DESCRIPTION_LENGTH( - 18), INVALID_DATA_LENGTH(27), INVALID_QUANTITY(28), ASSET_DOES_NOT_EXIST(29), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000); - - public final int value; - - private final static Map map = stream(ValidationResult.values()).collect(toMap(result -> result.value, result -> result)); - - ValidationResult(int value) { - this.value = value; - } - - public static ValidationResult valueOf(int value) { - return map.get(value); - } - } - - // Minimum fee - public static final BigDecimal MINIMUM_FEE = BigDecimal.ONE; - - // Cached info to make transaction processing faster - protected static final BigDecimal maxBytePerFee = BigDecimal.valueOf(Settings.getInstance().getMaxBytePerFee()); - protected static final BigDecimal minFeePerByte = BigDecimal.ONE.divide(maxBytePerFee, MathContext.DECIMAL32); - - // Database properties shared with all transaction types - protected TransactionType type; - protected PublicKeyAccount creator; - protected long timestamp; - protected byte[] reference; - protected BigDecimal fee; - protected byte[] signature; - - // Derived/cached properties - - // Property lengths for serialisation - protected static final int TYPE_LENGTH = 4; - protected static final int TIMESTAMP_LENGTH = 8; - public static final int REFERENCE_LENGTH = 64; - protected static final int FEE_LENGTH = 8; - public static final int SIGNATURE_LENGTH = 64; - protected static final int BASE_TYPELESS_LENGTH = TIMESTAMP_LENGTH + REFERENCE_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH; - - // Other length constants - public static final int CREATOR_LENGTH = 32; - public static final int RECIPIENT_LENGTH = 25; - - // Constructors - - protected Transaction(TransactionType type, BigDecimal fee, PublicKeyAccount creator, long timestamp, byte[] reference, byte[] signature) { - this.fee = fee; - this.type = type; - this.creator = creator; - this.timestamp = timestamp; - this.reference = reference; - this.signature = signature; - } - - protected Transaction(TransactionType type, BigDecimal fee, PublicKeyAccount creator, long timestamp, byte[] reference) { - this(type, fee, creator, timestamp, reference, null); - } - - // Getters/setters - - public TransactionType getType() { - return this.type; - } - - public PublicKeyAccount getCreator() { - return this.creator; - } - - public long getTimestamp() { - return this.timestamp; - } - - public byte[] getReference() { - return this.reference; - } - - public BigDecimal getFee() { - return this.fee; - } - - public byte[] getSignature() { - return this.signature; - } - - // More information - - public long getDeadline() { - // 24 hour deadline to include transaction in a block - return this.timestamp + (24 * 60 * 60 * 1000); - } - - /** - * Return length of byte[] if {@link Transactions#toBytes()} is called. - *

- * Used to allocate byte[]s or during serialization. - * - * @return length of serialized transaction - */ - public abstract int getDataLength(); - - public boolean hasMinimumFee() { - return this.fee.compareTo(MINIMUM_FEE) >= 0; - } - - public BigDecimal feePerByte() { - return this.fee.divide(new BigDecimal(this.getDataLength()), MathContext.DECIMAL32); - } - - public boolean hasMinimumFeePerByte() { - return this.feePerByte().compareTo(minFeePerByte) >= 0; - } - - public BigDecimal calcRecommendedFee() { - BigDecimal recommendedFee = BigDecimal.valueOf(this.getDataLength()).divide(maxBytePerFee, MathContext.DECIMAL32).setScale(8); - - // security margin - recommendedFee = recommendedFee.add(new BigDecimal("0.0000001")); - - if (recommendedFee.compareTo(MINIMUM_FEE) <= 0) { - recommendedFee = MINIMUM_FEE; - } else { - recommendedFee = recommendedFee.setScale(0, BigDecimal.ROUND_UP); - } - - return recommendedFee.setScale(8); - } - - public static int getVersionByTimestamp(long timestamp) { - if (timestamp < Block.POWFIX_RELEASE_TIMESTAMP) { - return 1; - } else { - return 3; - } - } - - /** - * Get block height for this transaction in the blockchain. - * - * @return height, or 0 if not in blockchain (i.e. unconfirmed) - * @throws SQLException - */ - public int getHeight() throws SQLException { - if (this.signature == null) - return 0; - - BlockTransaction blockTx = BlockTransaction.fromTransactionSignature(this.signature); - if (blockTx == null) - return 0; - - return BlockChain.getBlockHeightFromSignature(blockTx.getBlockSignature()); - } - - /** - * Get number of confirmations for this transaction. - * - * @return confirmation count, or 0 if not in blockchain (i.e. unconfirmed) - * @throws SQLException - */ - public int getConfirmations() throws SQLException { - int ourHeight = this.getHeight(); - if (ourHeight == 0) - return 0; - - int blockChainHeight = BlockChain.getHeight(); - return blockChainHeight - ourHeight + 1; - } - - // Load/Save/Delete - - // Typically called by sub-class' load-from-DB constructors - - /** - * Load base Transaction from DB using signature. - *

- * Note that the transaction type is not checked against the DB's value. - * - * @param type - * @param signature - * @throws NoDataFoundException - * if no matching row found - * @throws SQLException - */ - protected Transaction(TransactionType type, byte[] signature) throws SQLException { - ResultSet rs = DB.checkedExecute("SELECT reference, creator, creation, fee FROM Transactions WHERE signature = ? AND type = ?", signature, type.value); - if (rs == null) - throw new NoDataFoundException(); - - this.type = type; - this.reference = DB.getResultSetBytes(rs.getBinaryStream(1), REFERENCE_LENGTH); - // Note: can't use CREATOR_LENGTH in case we encounter Genesis Account's short, 8-byte public key - this.creator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(2))); - this.timestamp = rs.getTimestamp(3).getTime(); - this.fee = rs.getBigDecimal(4).setScale(8); - this.signature = signature; - } - - protected void save() throws SQLException { - SaveHelper saveHelper = new SaveHelper("Transactions"); - saveHelper.bind("signature", this.signature).bind("reference", this.reference).bind("type", this.type.value) - .bind("creator", this.creator.getPublicKey()).bind("creation", new Timestamp(this.timestamp)).bind("fee", this.fee) - .bind("milestone_block", null); - saveHelper.execute(); - } - - protected void delete() throws SQLException { - // NOTE: The corresponding row in sub-table is deleted automatically by the database thanks to "ON DELETE CASCADE" in the sub-table's FOREIGN KEY - // definition. - DB.checkedExecute("DELETE FROM Transactions WHERE signature = ?", this.signature); - } - - // Navigation - - /** - * Load encapsulating Block from DB, if any - * - * @return Block, or null if transaction is not in a Block - * @throws SQLException - */ - public Block getBlock() throws SQLException { - if (this.signature == null) - return null; - - BlockTransaction blockTx = BlockTransaction.fromTransactionSignature(this.signature); - if (blockTx == null) - return null; - - return Block.fromSignature(blockTx.getBlockSignature()); - } - - /** - * Load parent Transaction from DB via this transaction's reference. - * - * @return Transaction, or null if no parent found (which should not happen) - * @throws SQLException - */ - public Transaction getParent() throws SQLException { - if (this.reference == null) - return null; - - // return TransactionFactory.fromSignature(this.reference); - // return RepositoryManager.getTransactionRepository().fromSignature(this.reference); - } - - /** - * Load child Transaction from DB, if any. - * - * @return Transaction, or null if no child found - * @throws SQLException - */ - public Transaction getChild() throws SQLException { - if (this.signature == null) - return null; - - return TransactionFactory.fromReference(this.signature); - } - - // Converters - - /** - * Deserialize a byte[] into corresponding Transaction subclass. - * - * @param data - * @return subclass of Transaction, e.g. PaymentTransaction - * @throws ParseException - */ - public static Transaction parse(byte[] data) throws ParseException { - if (data == null) - return null; - - if (data.length < TYPE_LENGTH) - throw new ParseException("Byte data too short to determine transaction type"); - - ByteBuffer byteBuffer = ByteBuffer.wrap(data); - - TransactionType type = TransactionType.valueOf(byteBuffer.getInt()); - if (type == null) - return null; - - switch (type) { - case GENESIS: - return GenesisTransaction.parse(byteBuffer); - - case PAYMENT: - return PaymentTransaction.parse(byteBuffer); - - case MESSAGE: - return MessageTransaction.parse(byteBuffer); - - default: - return null; - } - } - - public abstract JSONObject toJSON() throws SQLException; - - /** - * Produce JSON representation of common/base Transaction info. - * - * @return JSONObject - * @throws SQLException - */ - @SuppressWarnings("unchecked") - protected JSONObject getBaseJSON() throws SQLException { - JSONObject json = new JSONObject(); - - json.put("type", this.type.value); - json.put("fee", this.fee.toPlainString()); - json.put("timestamp", this.timestamp); - json.put("signature", Base58.encode(this.signature)); - - if (this.reference != null) - json.put("reference", Base58.encode(this.reference)); - - json.put("confirmations", this.getConfirmations()); - - return json; - } - - /** - * Serialize transaction as byte[], including signature. - * - * @return byte[] - */ - public abstract byte[] toBytes(); - - /** - * Serialize transaction as byte[], stripping off trailing signature. - *

- * Used by signature-related methods such as {@link Transaction#calcSignature(PrivateKeyAccount)} and {@link Transaction#isSignatureValid()} - * - * @return byte[] - */ - private byte[] toBytesLessSignature() { - byte[] bytes = this.toBytes(); - return Arrays.copyOf(bytes, bytes.length - SIGNATURE_LENGTH); - } - - // Processing - - public byte[] calcSignature(PrivateKeyAccount signer) { - return signer.sign(this.toBytesLessSignature()); - } - - public boolean isSignatureValid() { - if (this.signature == null) - return false; - - return this.creator.verify(this.signature, this.toBytesLessSignature()); - } - - /** - * Returns whether transaction can be added to the blockchain. - *

- * Checks if transaction can have {@link Transaction#process()} called. - *

- * Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}. - *

- * Transactions that have already been processed will return false. - * - * @return true if transaction can be processed, false otherwise - * @throws SQLException - */ - public abstract ValidationResult isValid() throws SQLException; - - /** - * Actually process a transaction, updating the blockchain. - *

- * Processes transaction, updating balances, references, assets, etc. as appropriate. - *

- * Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}. - * - * @throws SQLException - */ - public abstract void process() throws SQLException; - - /** - * Undo transaction, updating the blockchain. - *

- * Undoes transaction, updating balances, references, assets, etc. as appropriate. - *

- * Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}. - * - * @throws SQLException - */ - public abstract void orphan() throws SQLException; - -} diff --git a/src/qora/transaction/TransactionHandler.java b/src/qora/transaction/TransactionHandler.java new file mode 100644 index 00000000..3353aee2 --- /dev/null +++ b/src/qora/transaction/TransactionHandler.java @@ -0,0 +1,238 @@ +package qora.transaction; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.Arrays; +import java.util.Map; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toMap; + +import org.json.simple.JSONObject; + +import data.transaction.Transaction; +import database.DB; +import database.NoDataFoundException; +import qora.account.PrivateKeyAccount; +import qora.account.PublicKeyAccount; +import qora.block.Block; +import qora.block.BlockChain; +import qora.block.BlockTransaction; +import repository.Repository; +import repository.RepositoryManager; +import settings.Settings; +import transform.TransformationException; +import transform.Transformer; +import transform.transaction.TransactionTransformer; +import utils.Base58; + +public abstract class TransactionHandler { + + // 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_DESCRIPTION_LENGTH( + 18), INVALID_DATA_LENGTH(27), INVALID_QUANTITY(28), ASSET_DOES_NOT_EXIST(29), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000); + + public final int value; + + private final static Map map = stream(ValidationResult.values()).collect(toMap(result -> result.value, result -> result)); + + ValidationResult(int value) { + this.value = value; + } + + public static ValidationResult valueOf(int value) { + return map.get(value); + } + } + + // Minimum fee + public static final BigDecimal MINIMUM_FEE = BigDecimal.ONE; + + // Cached info to make transaction processing faster + protected static final BigDecimal maxBytePerFee = BigDecimal.valueOf(Settings.getInstance().getMaxBytePerFee()); + protected static final BigDecimal minFeePerByte = BigDecimal.ONE.divide(maxBytePerFee, MathContext.DECIMAL32); + + private Transaction transaction; + + // Constructors + + public TransactionHandler(Transaction transaction) { + this.transaction = transaction; + } + + // More information + + + + public long getDeadline() { + // 24 hour deadline to include transaction in a block + return this.transaction.getTimestamp() + (24 * 60 * 60 * 1000); + } + + public boolean hasMinimumFee() { + return this.transaction.getFee().compareTo(MINIMUM_FEE) >= 0; + } + + public BigDecimal feePerByte() { + try { + return this.transaction.getFee().divide(new BigDecimal(TransactionTransformer.getDataLength(this.transaction)), MathContext.DECIMAL32); + } catch (TransformationException e) { + throw new IllegalStateException("Unable to get transaction byte length?"); + } + } + + public boolean hasMinimumFeePerByte() { + return this.feePerByte().compareTo(minFeePerByte) >= 0; + } + + public BigDecimal calcRecommendedFee() { + try { + BigDecimal recommendedFee = BigDecimal.valueOf(TransactionTransformer.getDataLength(this.transaction)).divide(maxBytePerFee, MathContext.DECIMAL32).setScale(8); + + // security margin + recommendedFee = recommendedFee.add(new BigDecimal("0.0000001")); + + if (recommendedFee.compareTo(MINIMUM_FEE) <= 0) { + recommendedFee = MINIMUM_FEE; + } else { + recommendedFee = recommendedFee.setScale(0, BigDecimal.ROUND_UP); + } + + return recommendedFee.setScale(8); + } catch (TransformationException e) { + throw new IllegalStateException("Unable to get transaction byte length?"); + } + } + + public static int getVersionByTimestamp(long timestamp) { + if (timestamp < Block.POWFIX_RELEASE_TIMESTAMP) { + return 1; + } else { + return 3; + } + } + + /** + * Get block height for this transaction in the blockchain. + * + * @return height, or 0 if not in blockchain (i.e. unconfirmed) + */ + public int getHeight() { + return RepositoryManager.getTransactionRepository().getHeight(this.transaction); + } + + /** + * Get number of confirmations for this transaction. + * + * @return confirmation count, or 0 if not in blockchain (i.e. unconfirmed) + */ + public int getConfirmations() { + int ourHeight = this.getHeight(); + if (ourHeight == 0) + return 0; + + int blockChainHeight = BlockChain.getHeight(); + return blockChainHeight - ourHeight + 1; + } + + // Navigation + + /** + * Load encapsulating Block from DB, if any + * + * @return Block, or null if transaction is not in a Block + */ + public Block getBlock() { + return RepositoryManager.getTransactionRepository().toBlock(this.transaction); + } + + /** + * Load parent Transaction from DB via this transaction's reference. + * + * @return Transaction, or null if no parent found (which should not happen) + */ + public Transaction getParent() { + byte[] reference = this.transaction.getReference(); + if (reference == null) + return null; + + return RepositoryManager.getTransactionRepository().fromSignature(reference); + } + + /** + * Load child Transaction from DB, if any. + * + * @return Transaction, or null if no child found + */ + public Transaction getChild() { + byte[] signature = this.transaction.getSignature(); + if (signature == null) + return null; + + return RepositoryManager.getTransactionRepository().fromSignature(signature); + } + + /** + * Serialize transaction as byte[], stripping off trailing signature. + *

+ * Used by signature-related methods such as {@link TransactionHandler#calcSignature(PrivateKeyAccount)} and {@link TransactionHandler#isSignatureValid()} + * + * @return byte[] + */ + private byte[] toBytesLessSignature() { + try { + byte[] bytes = TransactionTransformer.toBytes(this.transaction); + return Arrays.copyOf(bytes, bytes.length - Transformer.SIGNATURE_LENGTH); + } catch (TransformationException e) { + // XXX this isn't good + return null; + } + } + + // Processing + + public byte[] calcSignature(PrivateKeyAccount signer) { + return signer.sign(this.toBytesLessSignature()); + } + + public boolean isSignatureValid() { + byte[] signature = this.transaction.getSignature(); + if (signature == null) + return false; + + // XXX: return this.transaction.getCreator().verify(signature, this.toBytesLessSignature()); + return false; + } + + /** + * Returns whether transaction can be added to the blockchain. + *

+ * Checks if transaction can have {@link TransactionHandler#process()} called. + *

+ * Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}. + *

+ * Transactions that have already been processed will return false. + * + * @return true if transaction can be processed, false otherwise + */ + public abstract ValidationResult isValid(); + + /** + * Actually process a transaction, updating the blockchain. + *

+ * Processes transaction, updating balances, references, assets, etc. as appropriate. + *

+ * Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}. + */ + public abstract void process(); + + /** + * Undo transaction, updating the blockchain. + *

+ * Undoes transaction, updating balances, references, assets, etc. as appropriate. + *

+ * Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}. + */ + public abstract void orphan(); + +} diff --git a/src/repository/TransactionRepository.java b/src/repository/TransactionRepository.java index 74369d74..7ca1a15a 100644 --- a/src/repository/TransactionRepository.java +++ b/src/repository/TransactionRepository.java @@ -1,6 +1,7 @@ package repository; import data.transaction.Transaction; +import qora.block.Block; public interface TransactionRepository { @@ -8,4 +9,12 @@ public interface TransactionRepository { public Transaction fromReference(byte[] reference); + public int getHeight(Transaction transaction); + + public Block toBlock(Transaction transaction); + + public void save(Transaction transaction); + + public void delete(Transaction transaction); + } diff --git a/src/repository/hsqldb/HSQLDBGenesisTransaction.java b/src/repository/hsqldb/HSQLDBGenesisTransaction.java index f670541b..e7142b6d 100644 --- a/src/repository/hsqldb/HSQLDBGenesisTransaction.java +++ b/src/repository/hsqldb/HSQLDBGenesisTransaction.java @@ -12,7 +12,7 @@ import database.DB; public class HSQLDBGenesisTransaction extends HSQLDBTransaction { - protected Transaction fromBase(byte[] signature, byte[] reference, PublicKeyAccount creator, long timestamp, BigDecimal fee) { + Transaction fromBase(byte[] signature, byte[] reference, PublicKeyAccount creator, long timestamp, BigDecimal fee) { try { ResultSet rs = DB.checkedExecute("SELECT recipient, amount FROM GenesisTransactions WHERE signature = ?", signature); if (rs == null) diff --git a/src/database/SaveHelper.java b/src/repository/hsqldb/HSQLDBSaver.java similarity index 95% rename from src/database/SaveHelper.java rename to src/repository/hsqldb/HSQLDBSaver.java index 5aae6390..d6eff39d 100644 --- a/src/database/SaveHelper.java +++ b/src/repository/hsqldb/HSQLDBSaver.java @@ -1,4 +1,4 @@ -package database; +package repository.hsqldb; import java.math.BigDecimal; import java.sql.PreparedStatement; @@ -7,6 +7,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import database.DB; + /** * Database helper for building, and executing, INSERT INTO ... ON DUPLICATE KEY UPDATE ... statements. *

@@ -17,7 +19,7 @@ import java.util.List; * {@code helper.execute(); }
* */ -public class SaveHelper { +public class HSQLDBSaver { private String table; @@ -29,7 +31,7 @@ public class SaveHelper { * * @param table */ - public SaveHelper(String table) { + public HSQLDBSaver(String table) { this.table = table; } @@ -40,7 +42,7 @@ public class SaveHelper { * @param value * @return the same SaveHelper object */ - public SaveHelper bind(String column, Object value) { + public HSQLDBSaver bind(String column, Object value) { columns.add(column); objects.add(value); return this; diff --git a/src/repository/hsqldb/HSQLDBTransaction.java b/src/repository/hsqldb/HSQLDBTransaction.java index 8252589f..f4e79072 100644 --- a/src/repository/hsqldb/HSQLDBTransaction.java +++ b/src/repository/hsqldb/HSQLDBTransaction.java @@ -3,11 +3,13 @@ package repository.hsqldb; import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Timestamp; -import database.DB; import data.account.PublicKeyAccount; import data.transaction.Transaction; import data.transaction.Transaction.TransactionType; +import database.DB; +import qora.block.Block; import repository.TransactionRepository; public class HSQLDBTransaction implements TransactionRepository { @@ -64,4 +66,69 @@ public class HSQLDBTransaction implements TransactionRepository { } } + @Override + public int getHeight(Transaction transaction) { + byte[] signature = transaction.getSignature(); + if (signature == null) + return 0; + + // in one go? + try { + ResultSet rs = DB.checkedExecute("SELECT height from BlockTransactions JOIN Blocks ON Blocks.signature = BlockTransactions.block_signature WHERE transaction_signature = ? LIMIT 1", signature); + if (rs == null) + return 0; + + return rs.getInt(1); + } catch (SQLException e) { + return 0; + } + } + + @Override + public Block toBlock(Transaction transaction) { + byte[] signature = transaction.getSignature(); + if (signature == null) + return null; + + // Fetch block signature (if any) + try { + ResultSet rs = DB.checkedExecute("SELECT block_signature from BlockTransactions WHERE transaction_signature = ? LIMIT 1", signature); + if (rs == null) + return null; + + byte[] blockSignature = DB.getResultSetBytes(rs.getBinaryStream(1)); + + // TODO + // return RepositoryManager.getBlockRepository().fromSignature(blockSignature); + + return null; + } catch (SQLException e) { + return null; + } + } + + @Override + public void save(Transaction transaction) { + HSQLDBSaver saver = new HSQLDBSaver("Transactions"); + saver.bind("signature", transaction.getSignature()).bind("reference", transaction.getReference()).bind("type", transaction.getType().value) + .bind("creator", transaction.getCreator().getPublicKey()).bind("creation", new Timestamp(transaction.getTimestamp())).bind("fee", transaction.getFee()) + .bind("milestone_block", null); + try { + saver.execute(); + } catch (SQLException e) { + // XXX do what? + } + } + + @Override + public void delete(Transaction transaction) { + // NOTE: The corresponding row in sub-table is deleted automatically by the database thanks to "ON DELETE CASCADE" in the sub-table's FOREIGN KEY + // definition. + try { + DB.checkedExecute("DELETE FROM Transactions WHERE signature = ?", transaction.getSignature()); + } catch (SQLException e) { + // XXX do what? + } + } + } diff --git a/src/test/blocks.java b/src/test/blocks.java index 2c9d6b9d..eaf9e7a9 100644 --- a/src/test/blocks.java +++ b/src/test/blocks.java @@ -10,8 +10,8 @@ import org.junit.Test; import qora.block.Block; import qora.block.GenesisBlock; -import qora.transaction.Transaction; -import qora.transaction.TransactionFactory; +import qora.transaction.TransactionHandler; +import qora.transaction.TransactionHandler; public class blocks extends common { @@ -23,26 +23,26 @@ public class blocks extends common { // only true if blockchain is empty // assertTrue(block.isValid(connection)); - List transactions = block.getTransactions(); + List transactions = block.getTransactions(); assertNotNull(transactions); - for (Transaction transaction : transactions) { + for (TransactionHandler transaction : transactions) { assertNotNull(transaction); - assertEquals(Transaction.TransactionType.GENESIS, transaction.getType()); + assertEquals(Transaction.TransactionHandler.GENESIS, transaction.getType()); assertTrue(transaction.getFee().compareTo(BigDecimal.ZERO) == 0); assertNull(transaction.getReference()); assertTrue(transaction.isSignatureValid()); - assertEquals(Transaction.ValidationResult.OK, transaction.isValid()); + assertEquals(TransactionHandler.ValidationResult.OK, transaction.isValid()); } // Attempt to load first transaction directly from database - Transaction transaction = TransactionFactory.fromSignature(transactions.get(0).getSignature()); + TransactionHandler transaction = TransactionFactory.fromSignature(transactions.get(0).getSignature()); assertNotNull(transaction); - assertEquals(Transaction.TransactionType.GENESIS, transaction.getType()); + assertEquals(Transaction.TransactionHandler.GENESIS, transaction.getType()); assertTrue(transaction.getFee().compareTo(BigDecimal.ZERO) == 0); assertNull(transaction.getReference()); assertTrue(transaction.isSignatureValid()); - assertEquals(Transaction.ValidationResult.OK, transaction.isValid()); + assertEquals(TransactionHandler.ValidationResult.OK, transaction.isValid()); } @Test @@ -53,21 +53,21 @@ public class blocks extends common { assertNotNull("Block 754 is required for this test", block); assertTrue(block.isSignatureValid()); - List transactions = block.getTransactions(); + List transactions = block.getTransactions(); assertNotNull(transactions); - for (Transaction transaction : transactions) { + for (TransactionHandler transaction : transactions) { assertNotNull(transaction); - assertEquals(Transaction.TransactionType.PAYMENT, transaction.getType()); + assertEquals(Transaction.TransactionHandler.PAYMENT, transaction.getType()); assertFalse(transaction.getFee().compareTo(BigDecimal.ZERO) == 0); assertNotNull(transaction.getReference()); assertTrue(transaction.isSignatureValid()); } // Attempt to load first transaction directly from database - Transaction transaction = TransactionFactory.fromSignature(transactions.get(0).getSignature()); + TransactionHandler transaction = TransactionFactory.fromSignature(transactions.get(0).getSignature()); assertNotNull(transaction); - assertEquals(Transaction.TransactionType.PAYMENT, transaction.getType()); + assertEquals(Transaction.TransactionHandler.PAYMENT, transaction.getType()); assertFalse(transaction.getFee().compareTo(BigDecimal.ZERO) == 0); assertNotNull(transaction.getReference()); assertTrue(transaction.isSignatureValid()); diff --git a/src/test/load.java b/src/test/load.java index 471d126f..aed9c156 100644 --- a/src/test/load.java +++ b/src/test/load.java @@ -8,8 +8,8 @@ import org.junit.Test; import qora.block.BlockChain; import qora.transaction.PaymentTransaction; -import qora.transaction.Transaction; -import qora.transaction.TransactionFactory; +import qora.transaction.TransactionHandler; +import qora.transaction.TransactionHandler; import utils.Base58; public class load extends common { @@ -40,7 +40,7 @@ public class load extends common { byte[] signature = Base58.decode(signature58); while (true) { - Transaction transaction = TransactionFactory.fromSignature(signature); + TransactionHandler transaction = TransactionFactory.fromSignature(signature); if (transaction == null) break; diff --git a/src/test/migrate.java b/src/test/migrate.java index 3a8062bc..51cedaa1 100644 --- a/src/test/migrate.java +++ b/src/test/migrate.java @@ -31,7 +31,7 @@ import com.google.common.io.CharStreams; import database.DB; import qora.block.BlockChain; -import qora.transaction.Transaction; +import qora.transaction.TransactionHandler; import utils.Base58; public class migrate extends common { @@ -561,7 +561,7 @@ public class migrate extends common { } messagePStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - messagePStmt.setInt(2, Transaction.getVersionByTimestamp(transactionTimestamp)); + messagePStmt.setInt(2, TransactionHandler.getVersionByTimestamp(transactionTimestamp)); messagePStmt.setBinaryStream(3, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("creator")))); messagePStmt.setString(4, (String) transaction.get("recipient")); messagePStmt.setBoolean(5, isText); diff --git a/src/test/transactions.java b/src/test/transactions.java index 9b4fba7a..f6281655 100644 --- a/src/test/transactions.java +++ b/src/test/transactions.java @@ -11,13 +11,13 @@ import org.junit.Test; import qora.block.Block; import qora.block.GenesisBlock; import qora.transaction.GenesisTransaction; -import qora.transaction.Transaction; -import utils.ParseException; +import qora.transaction.TransactionHandler; +import transform.TransformationException; public class transactions extends common { @Test - public void testGenesisSerialization() throws SQLException, ParseException { + public void testGenesisSerialization() throws SQLException, TransformationException { GenesisBlock block = GenesisBlock.getInstance(); GenesisTransaction transaction = (GenesisTransaction) block.getTransactions().get(1); @@ -27,19 +27,19 @@ public class transactions extends common { byte[] bytes = transaction.toBytes(); - GenesisTransaction parsedTransaction = (GenesisTransaction) Transaction.parse(bytes); + GenesisTransaction parsedTransaction = (GenesisTransaction) TransactionHandler.parse(bytes); System.out.println(parsedTransaction.getTimestamp() + ": " + parsedTransaction.getRecipient().getAddress() + " received " + parsedTransaction.getAmount().toPlainString()); assertTrue(Arrays.equals(transaction.getSignature(), parsedTransaction.getSignature())); } - public void testGenericSerialization(Transaction transaction) throws SQLException, ParseException { + public void testGenericSerialization(TransactionHandler transaction) throws SQLException, TransformationException { assertNotNull(transaction); byte[] bytes = transaction.toBytes(); - Transaction parsedTransaction = Transaction.parse(bytes); + TransactionHandler parsedTransaction = TransactionHandler.parse(bytes); assertTrue(Arrays.equals(transaction.getSignature(), parsedTransaction.getSignature())); @@ -47,22 +47,22 @@ public class transactions extends common { } @Test - public void testPaymentSerialization() throws SQLException, ParseException { + public void testPaymentSerialization() throws SQLException, TransformationException { // Block 949 has lots of varied transactions // Blocks 390 & 754 have only payment transactions Block block = Block.fromHeight(754); assertNotNull("Block 754 is required for this test", block); assertTrue(block.isSignatureValid()); - List transactions = block.getTransactions(); + List transactions = block.getTransactions(); assertNotNull(transactions); - for (Transaction transaction : transactions) + for (TransactionHandler transaction : transactions) testGenericSerialization(transaction); } @Test - public void testMessageSerialization() throws SQLException, ParseException { + public void testMessageSerialization() throws SQLException, TransformationException { // Message transactions went live block 99000 // Some transactions to be found in block 99001/2/5/6 } diff --git a/src/transform/TransformationException.java b/src/transform/TransformationException.java new file mode 100644 index 00000000..f2d881d0 --- /dev/null +++ b/src/transform/TransformationException.java @@ -0,0 +1,14 @@ +package transform; + +@SuppressWarnings("serial") +public class TransformationException extends Exception { + + public TransformationException(String message) { + super(message); + } + + public TransformationException(Exception e) { + super(e); + } + +} diff --git a/src/transform/Transformer.java b/src/transform/Transformer.java new file mode 100644 index 00000000..e9dd7ec9 --- /dev/null +++ b/src/transform/Transformer.java @@ -0,0 +1,14 @@ +package transform; + +public abstract class Transformer { + + public static final int INT_LENGTH = 4; + public static final int LONG_LENGTH = 8; + + // Raw, not Base58-encoded + public static final int ADDRESS_LENGTH = 25; + + public static final int SIGNATURE_LENGTH = 64; + public static final int TIMESTAMP_LENGTH = LONG_LENGTH; + +} diff --git a/src/transform/transaction/GenesisTransactionTransformer.java b/src/transform/transaction/GenesisTransactionTransformer.java new file mode 100644 index 00000000..dab34ba8 --- /dev/null +++ b/src/transform/transaction/GenesisTransactionTransformer.java @@ -0,0 +1,76 @@ +package transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; + +import org.json.simple.JSONObject; + +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +import data.transaction.Transaction; +import data.account.Account; +import data.transaction.GenesisTransaction; +import transform.TransformationException; +import utils.Base58; +import utils.Serialization; + +public class GenesisTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH; + private static final int AMOUNT_LENGTH = LONG_LENGTH; + // Note that Genesis transactions don't require reference, fee or signature: + private static final int TYPELESS_LENGTH = TIMESTAMP_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH; + + static Transaction fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + if (byteBuffer.remaining() < TYPELESS_LENGTH) + throw new TransformationException("Byte data too short for GenesisTransaction"); + + long timestamp = byteBuffer.getLong(); + Account recipient = new Account(Serialization.deserializeRecipient(byteBuffer)); + BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); + + return new GenesisTransaction(recipient, amount, timestamp); + } + + public static int getDataLength(Transaction baseTransaction) throws TransformationException { + return TYPE_LENGTH + TYPELESS_LENGTH; + } + + public static byte[] toBytes(Transaction baseTransaction) throws TransformationException { + try { + GenesisTransaction transaction = (GenesisTransaction) baseTransaction; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(transaction.getType().value)); + bytes.write(Longs.toByteArray(transaction.getTimestamp())); + bytes.write(Base58.decode(transaction.getRecipient().getAddress())); + bytes.write(Serialization.serializeBigDecimal(transaction.getAmount())); + + return bytes.toByteArray(); + } catch (IOException | ClassCastException e) { + throw new TransformationException(e); + } + } + + @SuppressWarnings("unchecked") + public static JSONObject toJSON(Transaction baseTransaction) throws TransformationException { + JSONObject json = TransactionTransformer.getBaseJSON(baseTransaction); + + try { + GenesisTransaction transaction = (GenesisTransaction) baseTransaction; + + json.put("recipient", transaction.getRecipient().getAddress()); + json.put("amount", transaction.getAmount().toPlainString()); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/transform/transaction/TransactionTransformer.java b/src/transform/transaction/TransactionTransformer.java new file mode 100644 index 00000000..b5d6223f --- /dev/null +++ b/src/transform/transaction/TransactionTransformer.java @@ -0,0 +1,88 @@ +package transform.transaction; + +import java.nio.ByteBuffer; + +import org.json.simple.JSONObject; + +import data.transaction.Transaction; +import data.transaction.Transaction.TransactionType; +import transform.TransformationException; +import transform.Transformer; +import utils.Base58; + +public class TransactionTransformer extends Transformer { + + protected static final int TYPE_LENGTH = INT_LENGTH; + + public static Transaction fromBytes(byte[] bytes) throws TransformationException { + if (bytes == null) + return null; + + if (bytes.length < TYPE_LENGTH) + throw new TransformationException("Byte data too short to determine transaction type"); + + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + + TransactionType type = TransactionType.valueOf(byteBuffer.getInt()); + if (type == null) + return null; + + switch (type) { + case GENESIS: + return GenesisTransactionTransformer.fromByteBuffer(byteBuffer); + + default: + return null; + } + } + + public static int getDataLength(Transaction transaction) throws TransformationException { + switch (transaction.getType()) { + case GENESIS: + return GenesisTransactionTransformer.getDataLength(transaction); + + default: + throw new TransformationException("Unsupported transaction type"); + } + } + + public static byte[] toBytes(Transaction transaction) throws TransformationException { + switch (transaction.getType()) { + case GENESIS: + return GenesisTransactionTransformer.toBytes(transaction); + + default: + return null; + } + } + + public static JSONObject toJSON(Transaction transaction) throws TransformationException { + switch (transaction.getType()) { + case GENESIS: + return GenesisTransactionTransformer.toJSON(transaction); + + default: + return null; + } + } + + @SuppressWarnings("unchecked") + static JSONObject getBaseJSON(Transaction transaction) { + JSONObject json = new JSONObject(); + + json.put("type", transaction.getType().value); + json.put("fee", transaction.getFee().toPlainString()); + json.put("timestamp", transaction.getTimestamp()); + json.put("signature", Base58.encode(transaction.getSignature())); + + byte[] reference = transaction.getReference(); + if (reference != null) + json.put("reference", Base58.encode(reference)); + + // XXX Can't do this as it requires database access: + // json.put("confirmations", RepositoryManager.getTransactionRepository.getConfirmations(transaction)); + + return json; + } + +} diff --git a/src/utils/ParseException.java b/src/utils/ParseException.java deleted file mode 100644 index 03c6bef3..00000000 --- a/src/utils/ParseException.java +++ /dev/null @@ -1,10 +0,0 @@ -package utils; - -@SuppressWarnings("serial") -public class ParseException extends Exception { - - public ParseException(String message) { - super(message); - } - -} diff --git a/src/utils/Serialization.java b/src/utils/Serialization.java index 34a81732..37caa590 100644 --- a/src/utils/Serialization.java +++ b/src/utils/Serialization.java @@ -6,7 +6,8 @@ import java.math.BigInteger; import java.nio.ByteBuffer; import qora.account.PublicKeyAccount; -import qora.transaction.Transaction; +import qora.transaction.TransactionHandler; +import transform.TransformationException; public class Serialization { @@ -30,21 +31,21 @@ public class Serialization { } public static String deserializeRecipient(ByteBuffer byteBuffer) { - byte[] bytes = new byte[Transaction.RECIPIENT_LENGTH]; + byte[] bytes = new byte[TransactionHandler.RECIPIENT_LENGTH]; byteBuffer.get(bytes); return Base58.encode(bytes); } public static PublicKeyAccount deserializePublicKey(ByteBuffer byteBuffer) { - byte[] bytes = new byte[Transaction.CREATOR_LENGTH]; + byte[] bytes = new byte[TransactionHandler.CREATOR_LENGTH]; byteBuffer.get(bytes); return new PublicKeyAccount(bytes); } - public static String deserializeSizedString(ByteBuffer byteBuffer, int maxSize) throws ParseException { + public static String deserializeSizedString(ByteBuffer byteBuffer, int maxSize) throws TransformationException { int size = byteBuffer.getInt(); if (size > maxSize || size > byteBuffer.remaining()) - throw new ParseException("Serialized string too long"); + throw new TransformationException("Serialized string too long"); byte[] bytes = new byte[size]; byteBuffer.get(bytes); @@ -52,7 +53,7 @@ public class Serialization { try { return new String(bytes, "UTF-8"); } catch (UnsupportedEncodingException e) { - throw new ParseException("UTF-8 charset unsupported during string deserialization"); + throw new TransformationException("UTF-8 charset unsupported during string deserialization"); } }