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:
parent
3b02432b73
commit
3d78d5dad9
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
|
||||
}
|
238
src/qora/transaction/TransactionHandler.java
Normal file
238
src/qora/transaction/TransactionHandler.java
Normal 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();
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
@ -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?
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
}
|
||||
|
14
src/transform/TransformationException.java
Normal file
14
src/transform/TransformationException.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
14
src/transform/Transformer.java
Normal file
14
src/transform/Transformer.java
Normal 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;
|
||||
|
||||
}
|
76
src/transform/transaction/GenesisTransactionTransformer.java
Normal file
76
src/transform/transaction/GenesisTransactionTransformer.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
88
src/transform/transaction/TransactionTransformer.java
Normal file
88
src/transform/transaction/TransactionTransformer.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package utils;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class ParseException extends Exception {
|
||||
|
||||
public ParseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user