3
0
mirror of https://github.com/Qortal/qortal.git synced 2025-02-11 17:55:50 +00:00

More refactoring - DOES NOT COMPILE

qora.* packages are business logic/handler/processing
data.* packages are "value objects" or are they "business objects"?

toBytes(), fromBytes() (which used to be called parse()) and toJSON() moved to transform.* packages

new issues:

Lost control of SQL Transactions. Previously, some class "knew" whether to call COMMIT or not.
e.g. simply saving a payment transaction would involve updating Transactions table first, then the PaymentTransactions table after (to satisfy foreign key constraints) then commit.
Processing a block would involve a new transaction, a savepoint, a rollback and then maybe a further commit or rollback.
Not sure how this is going to work with the repository, especially if business logic isn't supposed to be aware of such things.

Growing number of stupid try-catch blocks. Probably best to ditch TransformationException (was ParseException) and throw IllegalStateExceptions instead as they're "unchecked".

What happens if the repository fails to save() to the database? It can't throw SQLException any more as that has no meaning outside the repository. Ditto with delete().
This commit is contained in:
catbref 2018-06-08 17:40:27 +01:00
parent 3b02432b73
commit 3d78d5dad9
28 changed files with 644 additions and 585 deletions

View File

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

View File

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

View File

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

View File

@ -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<Order> {
@ -109,7 +109,7 @@ public class Order implements Comparable<Order> {
// 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<Order> {
public Order copy() {
try {
return parse(this.toBytes());
} catch (ParseException e) {
} catch (TransformationException e) {
return null;
}
}

View File

@ -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<Transaction> transactions;
protected List<TransactionHandler> 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<Transaction>();
this.transactions = new ArrayList<TransactionHandler>();
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<Transaction> transactions) {
byte[] transactionsSignature, byte[] atBytes, BigDecimal atFees, List<TransactionHandler> 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<Transaction> getTransactions() throws SQLException {
public List<TransactionHandler> getTransactions() throws SQLException {
// Already loaded?
if (this.transactions != null)
return this.transactions;
// Allocate cache for results
this.transactions = new ArrayList<Transaction>();
this.transactions = new ArrayList<TransactionHandler>();
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<Trade> 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<Transaction> transactions = new ArrayList<Transaction>();
List<TransactionHandler> transactions = new ArrayList<TransactionHandler>();
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<Transaction> transactions = this.getTransactions();
for (Transaction transaction : transactions)
List<TransactionHandler> 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());

View File

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

View File

@ -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<Transaction>());
GENESIS_TRANSACTIONS_SIGNATURE, null, null, new ArrayList<TransactionHandler>());
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Integer, TransactionType> 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<Integer, ValidationResult> 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.
* <p>
* 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.
* <p>
* Note that the transaction type is <b>not</b> 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.
* <p>
* 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.
* <p>
* Checks if transaction can have {@link Transaction#process()} called.
* <p>
* Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}.
* <p>
* 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.
* <p>
* Processes transaction, updating balances, references, assets, etc. as appropriate.
* <p>
* 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.
* <p>
* Undoes transaction, updating balances, references, assets, etc. as appropriate.
* <p>
* Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}.
*
* @throws SQLException
*/
public abstract void orphan() throws SQLException;
}

View File

@ -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<Integer, ValidationResult> 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.
* <p>
* 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.
* <p>
* Checks if transaction can have {@link TransactionHandler#process()} called.
* <p>
* Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}.
* <p>
* 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.
* <p>
* Processes transaction, updating balances, references, assets, etc. as appropriate.
* <p>
* Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}.
*/
public abstract void process();
/**
* Undo transaction, updating the blockchain.
* <p>
* Undoes transaction, updating balances, references, assets, etc. as appropriate.
* <p>
* Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}.
*/
public abstract void orphan();
}

View File

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

View File

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

View File

@ -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.
* <p>
@ -17,7 +19,7 @@ import java.util.List;
* {@code helper.execute(); }<br>
*
*/
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;

View File

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

View File

@ -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<Transaction> transactions = block.getTransactions();
List<TransactionHandler> 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<Transaction> transactions = block.getTransactions();
List<TransactionHandler> 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());

View File

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

View File

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

View File

@ -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<Transaction> transactions = block.getTransactions();
List<TransactionHandler> 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
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +0,0 @@
package utils;
@SuppressWarnings("serial")
public class ParseException extends Exception {
public ParseException(String message) {
super(message);
}
}

View File

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