More database work

No need for DB.executeUsingBytes as it was only a specific use-case for DB.checkedExecute.
Callers now refactored to use DB.checkedExecute instead.
Minor tidying up of BlockTransactions in light of above.

In the HSQLDB database, asset keys/IDs are now "asset_id" (previously: "asset").
Added initial Asset/Order/Trade classes.
Added CreateOrderTransaction class.
Renamed some asset-related fields back to old gen1 names, e.g. haveAmount -> amount, wantAmount -> price.

Added Accounts and AccountBalances to database.

Added get/set confirmed balance support to Account.
Added get/set last reference support to Account.

Added Block.toJSON() - untested at this time.

Fleshed out some Transaction sub-classes' process() and orphan() methods.
Fleshed out PaymentTransaction.isValid().
Added Transaction.delete() - untested.
This commit is contained in:
catbref 2018-05-27 14:59:30 +01:00
parent 216ed7c772
commit 4ce499c444
18 changed files with 658 additions and 115 deletions

View File

@ -1,8 +1,8 @@
package database;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
@ -121,6 +121,7 @@ public class DB {
* @return byte[length]
*/
public static byte[] getResultSetBytes(InputStream inputStream, int length) {
// inputStream could be null if database's column's value is null
if (inputStream == null)
return null;
@ -145,6 +146,7 @@ public class DB {
public static byte[] getResultSetBytes(InputStream inputStream) {
final int BYTE_BUFFER_LENGTH = 1024;
// inputStream could be null if database's column's value is null
if (inputStream == null)
return null;
@ -168,33 +170,27 @@ public class DB {
}
/**
* Execute SQL using byte[] as 1st placeholder.
* Execute SQL and return ResultSet with but added checking.
* <p>
* <b>Note: calls ResultSet.next()</b> therefore returned ResultSet is already pointing to first row.
* <p>
* Typically used to fetch Blocks or Transactions using signature or reference.
*
* @param sql
* @param bytes
* @return ResultSet, or null if no matching rows found
* @param objects
* @return ResultSet, or null if there are no found rows
* @throws SQLException
*/
public static ResultSet executeUsingBytes(String sql, byte[] bytes) throws SQLException {
public static ResultSet checkedExecute(String sql, Object... objects) throws SQLException {
try (final Connection connection = DB.getConnection()) {
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setBinaryStream(1, new ByteArrayInputStream(bytes));
for (int i = 0; i < objects.length; ++i)
// Special treatment for BigDecimals so that they retain their "scale",
// which would otherwise be assumed as 0.
if (objects[i] instanceof BigDecimal)
preparedStatement.setBigDecimal(i + 1, (BigDecimal) objects[i]);
else
preparedStatement.setObject(i + 1, objects[i]);
if (!preparedStatement.execute())
throw new SQLException("Fetching from database produced no results");
ResultSet rs = preparedStatement.getResultSet();
if (rs == null)
throw new SQLException("Fetching results from database produced no ResultSet");
if (!rs.next())
return null;
return rs;
return checkedExecute(preparedStatement);
}
}

View File

@ -206,8 +206,8 @@ public class DatabaseUpdates {
case 12:
// Arbitrary/Multi-payment Transaction Payments
stmt.execute("CREATE TABLE SharedTransactionPayments (signature Signature, recipient QoraPublicKey NOT NULL, "
+ "amount QoraAmount NOT NULL, asset AssetID NOT NULL, "
+ "PRIMARY KEY (signature, recipient, asset), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
+ "amount QoraAmount NOT NULL, asset_id AssetID NOT NULL, "
+ "PRIMARY KEY (signature, recipient, asset_id), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
case 13:
@ -230,14 +230,14 @@ public class DatabaseUpdates {
case 15:
// Transfer Asset Transactions
stmt.execute("CREATE TABLE TransferAssetTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, "
+ "asset AssetID NOT NULL, amount QoraAmount NOT NULL, "
+ "asset_id AssetID NOT NULL, amount QoraAmount NOT NULL, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
case 16:
// Create Asset Order Transactions
stmt.execute("CREATE TABLE CreateAssetOrderTransactions (signature Signature, creator QoraPublicKey NOT NULL, "
+ "have_asset AssetID NOT NULL, have_amount QoraAmount NOT NULL, want_asset AssetID NOT NULL, want_amount QoraAmount NOT NULL, "
+ "have_asset_id AssetID NOT NULL, amount QoraAmount NOT NULL, want_asset_id AssetID NOT NULL, price QoraAmount NOT NULL, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
@ -265,20 +265,22 @@ public class DatabaseUpdates {
case 20:
// Message Transactions
stmt.execute("CREATE TABLE MessageTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, "
+ "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, amount QoraAmount NOT NULL, asset AssetID NOT NULL, data VARBINARY(4000) NOT NULL, "
+ "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, amount QoraAmount NOT NULL, asset_id AssetID NOT NULL, data VARBINARY(4000) NOT NULL, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
case 21:
// Assets (including QORA coin itself)
stmt.execute(
"CREATE TABLE Assets (asset AssetID IDENTITY, owner QoraAddress NOT NULL, asset_name AssetName NOT NULL, description VARCHAR(4000) NOT NULL, "
"CREATE TABLE Assets (asset_id AssetID IDENTITY, owner QoraAddress NOT NULL, asset_name AssetName NOT NULL, description VARCHAR(4000) NOT NULL, "
+ "quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, reference Signature NOT NULL)");
break;
case 22:
// Accounts
stmt.execute("CREATE TABLE AccountBalances (account QoraAddress, asset AssetID, amount QoraAmount NOT NULL, PRIMARY KEY (account, asset))");
stmt.execute("CREATE TABLE Accounts (account QoraAddress, reference Signature, PRIMARY KEY (account))");
stmt.execute(
"CREATE TABLE AccountBalances (account QoraAddress, asset_id AssetID, balance QoraAmount NOT NULL, PRIMARY KEY (account, asset_id))");
break;
default:

View File

@ -2,6 +2,11 @@ package qora.account;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import database.DB;
import database.SaveHelper;
public class Account {
@ -28,39 +33,60 @@ public class Account {
return this.getAddress().equals(((Account) b).getAddress());
}
// Balance manipulations - "key" is asset ID, or 0 for QORA
// Balance manipulations - assetId is 0 for QORA
public BigDecimal getBalance(long key, int confirmations) {
public BigDecimal getBalance(long assetId, int confirmations) {
// TODO
return null;
}
public BigDecimal getUnconfirmedBalance(long key) {
public BigDecimal getUnconfirmedBalance(long assetId) {
// TODO
return null;
}
public BigDecimal getConfirmedBalance(long key) {
// TODO
return null;
public BigDecimal getConfirmedBalance(long assetId) throws SQLException {
ResultSet resultSet = DB.checkedExecute("SELECT balance FROM AccountBalances WHERE account = ? and asset_id = ?", this.getAddress(), assetId);
if (resultSet == null)
return BigDecimal.ZERO.setScale(8);
return resultSet.getBigDecimal(1);
}
public void setConfirmedBalance(Connection connection, long key, BigDecimal amount) {
// TODO
return;
public void setConfirmedBalance(Connection connection, long assetId, BigDecimal balance) throws SQLException {
SaveHelper saveHelper = new SaveHelper(connection, "AccountBalances");
saveHelper.bind("account", this.getAddress()).bind("asset_id", assetId).bind("balance", balance);
saveHelper.execute();
}
// Reference manipulations
public byte[] getLastReference() {
// TODO
return null;
/**
* Fetch last reference for account.
*
* @return byte[] reference, or null if no reference or account not found.
* @throws SQLException
*/
public byte[] getLastReference() throws SQLException {
ResultSet resultSet = DB.checkedExecute("SELECT reference FROM Accounts WHERE account = ?", this.getAddress());
if (resultSet == null)
return null;
return DB.getResultSetBytes(resultSet.getBinaryStream(1));
}
// pass null to remove
public void setLastReference(Connection connection, byte[] reference) {
// TODO
return;
/**
* Set last reference for account.
*
* @param connection
* @param reference
* -- null allowed
* @throws SQLException
*/
public void setLastReference(Connection connection, byte[] reference) throws SQLException {
SaveHelper saveHelper = new SaveHelper(connection, "Accounts");
saveHelper.bind("account", this.getAddress()).bind("reference", reference);
saveHelper.execute();
}
}

View File

@ -7,12 +7,20 @@ import database.DB;
import database.SaveHelper;
import qora.account.PublicKeyAccount;
/*
* TODO:
* Probably need to standardize on using assetId or assetKey for the long value, and plain "asset" for the java object.
* Thus in the database the primary key column could be called "asset_id".
* In the Order object, we'd pass longs to variables with names like "haveAssetId" and use getters like "getHaveAssetId"
* which frees up other method names like "getHaveAsset" to return a java Asset object.
*/
public class Asset {
public static final long QORA = 0L;
// Properties
private Long key;
private Long assetId;
private PublicKeyAccount owner;
private String name;
private String description;
@ -20,8 +28,9 @@ public class Asset {
private boolean isDivisible;
private byte[] reference;
public Asset(Long key, PublicKeyAccount owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) {
this.key = key;
// NOTE: key is Long because it can be null if asset ID/key not yet assigned (which is done by save() method).
public Asset(Long assetId, PublicKeyAccount owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) {
this.assetId = assetId;
this.owner = owner;
this.name = name;
this.description = description;
@ -30,6 +39,7 @@ public class Asset {
this.reference = reference;
}
// New asset with unassigned assetId
public Asset(PublicKeyAccount owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) {
this(null, owner, name, description, quantity, isDivisible, reference);
}
@ -38,11 +48,11 @@ public class Asset {
public void save(Connection connection) throws SQLException {
SaveHelper saveHelper = new SaveHelper(connection, "Assets");
saveHelper.bind("asset", this.key).bind("owner", this.owner.getAddress()).bind("asset_name", this.name).bind("description", this.description)
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();
if (this.key == null)
this.key = DB.callIdentity(connection);
if (this.assetId == null)
this.assetId = DB.callIdentity(connection);
}
}

141
src/qora/assets/Order.java Normal file
View File

@ -0,0 +1,141 @@
package qora.assets;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;
import qora.account.Account;
import utils.ParseException;
public class Order implements Comparable<Order> {
// Properties
private BigInteger id;
private Account creator;
private long haveAssetId;
private long wantAssetId;
private BigDecimal amount;
private BigDecimal price;
private long timestamp;
// Other properties
private BigDecimal fulfilled;
// Constructors
public Order(BigInteger id, Account creator, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal price, long timestamp) {
this.id = id;
this.creator = creator;
this.haveAssetId = haveAssetId;
this.wantAssetId = wantAssetId;
this.amount = amount;
this.price = price;
this.timestamp = timestamp;
this.fulfilled = BigDecimal.ZERO.setScale(8);
}
public Order(BigInteger id, Account creator, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal price,
long timestamp) {
this(id, creator, haveAssetId, wantAssetId, amount, price, timestamp);
this.fulfilled = fulfilled;
}
// Getters/setters
public BigInteger getId() {
return this.id;
}
public Account getCreator() {
return creator;
}
public long getHaveAssetId() {
return haveAssetId;
}
public long getWantAssetId() {
return wantAssetId;
}
public BigDecimal getAmount() {
return amount;
}
public BigDecimal getPrice() {
return price;
}
public long getTimestamp() {
return timestamp;
}
public BigDecimal getFulfilled() {
return fulfilled;
}
public void setFulfilled(BigDecimal fulfilled) {
this.fulfilled = fulfilled;
}
// More information
public BigDecimal getAmountLeft() {
return this.amount.subtract(this.fulfilled);
}
public boolean isFulfilled() {
return this.fulfilled.compareTo(this.amount) == 0;
}
// TODO
// public List<Trade> getInitiatedTrades() {}
// TODO
// public boolean isConfirmed() {}
// Load/Save/Delete
// Navigation
// XXX is this getInitiatedTrades() above?
public List<Trade> getTrades() {
// TODO
return null;
}
// Converters
public static Order parse(byte[] data) throws ParseException {
// TODO
return null;
}
public byte[] toBytes() {
// TODO
return null;
}
// Processing
// Other
@Override
public int compareTo(Order order) {
// Compare using prices
return this.price.compareTo(order.getPrice());
}
public Order copy() {
try {
return parse(this.toBytes());
} catch (ParseException e) {
return null;
}
}
}

View File

@ -0,0 +1,47 @@
package qora.assets;
import java.math.BigDecimal;
import java.math.BigInteger;
public class Trade {
// Properties
private BigInteger initiator;
private BigInteger target;
private BigDecimal amount;
private BigDecimal price;
private long timestamp;
// Constructors
public Trade(BigInteger initiator, BigInteger target, BigDecimal amount, BigDecimal price, long timestamp) {
this.initiator = initiator;
this.target = target;
this.amount = amount;
this.price = price;
this.timestamp = timestamp;
}
// Getters/setters
public BigInteger getInitiator() {
return initiator;
}
public BigInteger getTarget() {
return target;
}
public BigDecimal getAmount() {
return amount;
}
public BigDecimal getPrice() {
return price;
}
public long getTimestamp() {
return timestamp;
}
}

View File

@ -12,9 +12,12 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import database.DB;
@ -23,8 +26,13 @@ import database.SaveHelper;
import qora.account.PrivateKeyAccount;
import qora.account.PublicKeyAccount;
import qora.assets.Asset;
import qora.assets.Order;
import qora.assets.Trade;
import qora.transaction.CreateOrderTransaction;
import qora.transaction.Transaction;
import qora.transaction.TransactionFactory;
import utils.Base58;
import utils.ParseException;
/*
* Typical use-case scenarios:
@ -215,11 +223,11 @@ public class Block {
// Allocate cache for results
this.transactions = new ArrayList<Transaction>();
ResultSet rs = DB.executeUsingBytes("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ?", this.getSignature());
ResultSet rs = DB.checkedExecute("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ?", this.getSignature());
if (rs == null)
return this.transactions; // No transactions in this block
// NB: do-while loop because DB.executeUsingBytes() implicitly calls ResultSet.next() for us
// NB: do-while loop because DB.checkedExecute() implicitly calls ResultSet.next() for us
do {
byte[] transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), Transaction.SIGNATURE_LENGTH);
this.transactions.add(TransactionFactory.fromSignature(transactionSignature));
@ -233,7 +241,7 @@ public class Block {
// Load/Save
protected Block(byte[] signature) throws SQLException {
this(DB.executeUsingBytes("SELECT " + DB_COLUMNS + " FROM Blocks WHERE signature = ?", signature));
this(DB.checkedExecute("SELECT " + DB_COLUMNS + " FROM Blocks WHERE signature = ?", signature));
}
protected Block(ResultSet rs) throws SQLException {
@ -329,7 +337,7 @@ public class Block {
if (blockSignature == null)
return null;
ResultSet resultSet = DB.executeUsingBytes("SELECT " + DB_COLUMNS + " FROM Blocks WHERE reference = ?", blockSignature);
ResultSet resultSet = DB.checkedExecute("SELECT " + DB_COLUMNS + " FROM Blocks WHERE reference = ?", blockSignature);
try {
return new Block(resultSet);
@ -340,12 +348,95 @@ public class Block {
// Converters
public JSONObject toJSON() {
// TODO
return null;
@SuppressWarnings("unchecked")
public JSONObject toJSON() throws SQLException {
JSONObject json = new JSONObject();
json.put("version", this.version);
json.put("timestamp", this.timestamp);
json.put("generatingBalance", this.generatingBalance);
json.put("generator", this.generator.getAddress());
json.put("generatorPublicKey", Base58.encode(this.generator.getPublicKey()));
json.put("fee", this.getTotalFees().toPlainString());
json.put("transactionsSignature", Base58.encode(this.transactionsSignature));
json.put("generatorSignature", Base58.encode(this.generatorSignature));
json.put("signature", Base58.encode(this.getSignature()));
if (this.reference != null)
json.put("reference", Base58.encode(this.reference));
json.put("height", this.getHeight());
// Add transaction info
JSONArray transactionsJson = new JSONArray();
boolean tradesHappened = false;
for (Transaction 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) {
CreateOrderTransaction orderTransaction = (CreateOrderTransaction) transaction;
Order order = orderTransaction.getOrder();
List<Trade> trades = order.getTrades();
// Filter out trades with timestamps that don't match order transaction's timestamp
trades.removeIf((Trade trade) -> trade.getTimestamp() != order.getTimestamp());
// Any trades left?
if (!trades.isEmpty()) {
tradesHappened = true;
// No need to check any further
break;
}
}
}
json.put("transactions", transactionsJson);
// Add asset trade activity flag
json.put("assetTrades", tradesHappened);
// Add CIYAM AT info (if any)
if (atBytes != null) {
json.put("blockATs", HashCode.fromBytes(atBytes).toString());
json.put("atFees", this.atFees);
}
return json;
}
public byte[] toBytes() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(getDataLength());
bytes.write(Ints.toByteArray(this.version));
bytes.write(Longs.toByteArray(this.timestamp));
bytes.write(this.reference);
// NOTE: generatingBalance serialized as long value, not as BigDecimal, for historic compatibility
bytes.write(Longs.toByteArray(this.generatingBalance.longValue()));
bytes.write(this.generator.getPublicKey());
bytes.write(this.transactionsSignature);
bytes.write(this.generatorSignature);
if (this.version >= 2) {
if (this.atBytes != null) {
bytes.write(Ints.toByteArray(this.atBytes.length));
bytes.write(this.atBytes);
// NOTE: atFees serialized as long value, not as BigDecimal, for historic compatibility
bytes.write(Longs.toByteArray(this.atFees.longValue()));
} else {
bytes.write(Ints.toByteArray(0));
bytes.write(Longs.toByteArray(0L));
}
}
return bytes.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static Block parse(byte[] data) throws ParseException {
// TODO
return null;
}

View File

@ -61,7 +61,7 @@ public class BlockChain {
* @throws SQLException
*/
public static int getBlockHeightFromSignature(byte[] signature) throws SQLException {
ResultSet rs = DB.executeUsingBytes("SELECT height FROM Blocks WHERE signature = ?", signature);
ResultSet rs = DB.checkedExecute("SELECT height FROM Blocks WHERE signature = ?", signature);
if (rs == null)
return 0;

View File

@ -1,12 +1,9 @@
package qora.block;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.io.ByteArrayInputStream;
import org.json.simple.JSONObject;
import database.DB;
@ -50,13 +47,8 @@ public class BlockTransaction {
protected BlockTransaction(byte[] blockSignature, int sequence) throws SQLException {
try (final Connection connection = DB.getConnection()) {
// Can't use DB.executeUsingBytes() here as we need two placeholders
PreparedStatement preparedStatement = connection
.prepareStatement("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ? and sequence = ?");
preparedStatement.setBinaryStream(1, new ByteArrayInputStream(blockSignature));
preparedStatement.setInt(2, sequence);
ResultSet rs = DB.checkedExecute(preparedStatement);
ResultSet rs = DB.checkedExecute("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ? and sequence = ?", blockSignature,
sequence);
if (rs == null)
throw new NoDataFoundException();
@ -67,7 +59,7 @@ public class BlockTransaction {
}
protected BlockTransaction(byte[] transactionSignature) throws SQLException {
ResultSet rs = DB.executeUsingBytes("SELECT block_signature, sequence FROM BlockTransactions WHERE transaction_signature = ?", transactionSignature);
ResultSet rs = DB.checkedExecute("SELECT block_signature, sequence FROM BlockTransactions WHERE transaction_signature = ?", transactionSignature);
if (rs == null)
throw new NoDataFoundException();

View File

@ -0,0 +1,165 @@
package qora.transaction;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.json.simple.JSONObject;
import com.google.common.primitives.Ints;
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;
public class CreateOrderTransaction extends Transaction {
// Properties
private Order order;
// Property lengths
private static final int ASSET_LENGTH = 8;
private static final int AMOUNT_LENGTH = 12;
private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + CREATOR_LENGTH + (ASSET_LENGTH + AMOUNT_LENGTH) * 2;
// Constructors
public CreateOrderTransaction(PublicKeyAccount creator, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal price, BigDecimal fee,
long timestamp, byte[] reference, byte[] signature) {
super(TransactionType.CREATE_ASSET_ORDER, fee, creator, timestamp, reference, signature);
this.order = new Order(new BigInteger(this.signature), creator, haveAssetId, wantAssetId, amount, price, timestamp);
}
public CreateOrderTransaction(PublicKeyAccount creator, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal price, BigDecimal fee,
long timestamp, byte[] reference) {
this(creator, haveAssetId, wantAssetId, amount, price, fee, timestamp, reference, null);
}
// Getters/Setters
public Order getOrder() {
return this.order;
}
// More information
public int getDataLength() {
return TYPE_LENGTH + TYPELESS_LENGTH;
}
// Load/Save
/**
* Load CreateOrderTransaction from DB using signature.
*
* @param signature
* @throws NoDataFoundException
* if no matching row found
* @throws SQLException
*/
protected CreateOrderTransaction(byte[] signature) throws SQLException {
super(TransactionType.CREATE_ASSET_ORDER, signature);
ResultSet rs = DB.checkedExecute("SELECT have_asset_id, amount, want_asset_id, price FROM CreateOrderTransactions WHERE signature = ?", signature);
if (rs == null)
throw new NoDataFoundException();
long haveAssetId = rs.getLong(1);
BigDecimal amount = rs.getBigDecimal(2);
long wantAssetId = rs.getLong(3);
BigDecimal price = rs.getBigDecimal(4);
this.order = new Order(new BigInteger(this.signature), this.creator, haveAssetId, wantAssetId, amount, price, this.timestamp);
}
/**
* Load CreateOrderTransaction from DB using signature
*
* @param signature
* @return CreateOrderTransaction, or null if not found
* @throws SQLException
*/
public static CreateOrderTransaction fromSignature(byte[] signature) throws SQLException {
try {
return new CreateOrderTransaction(signature);
} catch (NoDataFoundException e) {
return null;
}
}
@Override
public void save(Connection connection) throws SQLException {
super.save(connection);
SaveHelper saveHelper = new SaveHelper(connection, "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();
}
// Converters
protected static Transaction parse(ByteBuffer byteBuffer) throws ParseException {
if (byteBuffer.remaining() < TYPELESS_LENGTH)
throw new ParseException("Byte data too short for CreateOrderTransaction");
// TODO
return null;
}
@Override
public JSONObject toJSON() throws SQLException {
JSONObject json = getBaseJSON();
// TODO
return json;
}
public byte[] toBytes() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(getDataLength());
bytes.write(Ints.toByteArray(this.type.value));
bytes.write(Longs.toByteArray(this.timestamp));
bytes.write(this.reference);
// TODO
bytes.write(this.signature);
return bytes.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// Processing
public ValidationResult isValid(Connection connection) throws SQLException {
// TODO
return ValidationResult.OK;
}
public void process(Connection connection) throws SQLException {
this.save(connection);
// TODO
}
public void orphan(Connection connection) throws SQLException {
this.delete(connection);
// TODO
}
}

View File

@ -21,8 +21,10 @@ import database.SaveHelper;
import qora.account.Account;
import qora.account.GenesisAccount;
import qora.account.PrivateKeyAccount;
import qora.assets.Asset;
import qora.crypto.Crypto;
import utils.Base58;
import utils.ParseException;
import utils.Serialization;
public class GenesisTransaction extends Transaction {
@ -59,6 +61,7 @@ public class GenesisTransaction extends Transaction {
// More information
@Override
public int getDataLength() {
return TYPE_LENGTH + TYPELESS_LENGTH;
}
@ -76,7 +79,7 @@ public class GenesisTransaction extends Transaction {
protected GenesisTransaction(byte[] signature) throws SQLException {
super(TransactionType.GENESIS, signature);
ResultSet rs = DB.executeUsingBytes("SELECT recipient, amount FROM GenesisTransactions WHERE signature = ?", signature);
ResultSet rs = DB.checkedExecute("SELECT recipient, amount FROM GenesisTransactions WHERE signature = ?", signature);
if (rs == null)
throw new NoDataFoundException();
@ -110,9 +113,9 @@ public class GenesisTransaction extends Transaction {
// Converters
protected static Transaction parse(ByteBuffer byteBuffer) throws TransactionParseException {
protected static Transaction parse(ByteBuffer byteBuffer) throws ParseException {
if (byteBuffer.remaining() < TYPELESS_LENGTH)
throw new TransactionParseException("Byte data too short for GenesisTransaction");
throw new ParseException("Byte data too short for GenesisTransaction");
long timestamp = byteBuffer.getLong();
String recipient = Serialization.deserializeRecipient(byteBuffer);
@ -132,6 +135,7 @@ public class GenesisTransaction extends Transaction {
return json;
}
@Override
public byte[] toBytes() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(getDataLength());
@ -189,6 +193,7 @@ public class GenesisTransaction extends Transaction {
return Arrays.equals(this.signature, calcSignature());
}
@Override
public ValidationResult isValid(Connection connection) {
// Check amount is zero or positive
if (this.amount.compareTo(BigDecimal.ZERO) == -1)
@ -201,19 +206,26 @@ public class GenesisTransaction extends Transaction {
return ValidationResult.OK;
}
@Override
public void process(Connection connection) throws SQLException {
// TODO
this.save(connection);
// SET recipient's balance
// this.recipient.setConfirmedBalance(this.amount, db);
// Set recipient's balance
this.recipient.setConfirmedBalance(connection, Asset.QORA, this.amount);
// Set recipient's reference
// recipient.setLastReference(this.signature, db);
recipient.setLastReference(connection, this.signature);
}
public void orphan(Connection connection) {
// TODO
@Override
public void orphan(Connection connection) throws SQLException {
this.delete(connection);
// Set recipient's balance
this.recipient.setConfirmedBalance(connection, Asset.QORA, BigDecimal.ZERO);
// Set recipient's reference
recipient.setLastReference(connection, null);
}
}

View File

@ -7,6 +7,7 @@ import java.nio.ByteBuffer;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import org.json.simple.JSONObject;
@ -19,7 +20,10 @@ import database.NoDataFoundException;
import database.SaveHelper;
import qora.account.Account;
import qora.account.PublicKeyAccount;
import qora.assets.Asset;
import qora.crypto.Crypto;
import utils.Base58;
import utils.ParseException;
import utils.Serialization;
public class PaymentTransaction extends Transaction {
@ -82,7 +86,7 @@ public class PaymentTransaction extends Transaction {
protected PaymentTransaction(byte[] signature) throws SQLException {
super(TransactionType.PAYMENT, signature);
ResultSet rs = DB.executeUsingBytes("SELECT sender, recipient, amount FROM PaymentTransactions WHERE signature = ?", signature);
ResultSet rs = DB.checkedExecute("SELECT sender, recipient, amount FROM PaymentTransactions WHERE signature = ?", signature);
if (rs == null)
throw new NoDataFoundException();
@ -111,16 +115,16 @@ public class PaymentTransaction extends Transaction {
super.save(connection);
SaveHelper saveHelper = new SaveHelper(connection, "PaymentTransactions");
saveHelper.bind("signature", this.signature).bind("sender", this.sender.getPublicKey()).bind("recipient", this.recipient.getAddress()).bind("amount", this.amount);
saveHelper.bind("signature", this.signature).bind("sender", this.sender.getPublicKey()).bind("recipient", this.recipient.getAddress()).bind("amount",
this.amount);
saveHelper.execute();
}
// Converters
protected static Transaction parse(ByteBuffer byteBuffer) throws TransactionParseException {
// TODO
protected static Transaction parse(ByteBuffer byteBuffer) throws ParseException {
if (byteBuffer.remaining() < TYPELESS_LENGTH)
throw new TransactionParseException("Byte data too short for PaymentTransaction");
throw new ParseException("Byte data too short for PaymentTransaction");
long timestamp = byteBuffer.getLong();
byte[] reference = new byte[REFERENCE_LENGTH];
@ -167,17 +171,67 @@ public class PaymentTransaction extends Transaction {
// Processing
public ValidationResult isValid(Connection connection) {
// TODO
public ValidationResult isValid(Connection connection) throws SQLException {
// Non-database checks first
// Check recipient is a valid address
if (!Crypto.isValidAddress(this.recipient.getAddress()))
return ValidationResult.INVALID_ADDRESS;
// Check amount is positive
if (this.amount.compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_AMOUNT;
// Check fee is positive
if (this.fee.compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_FEE;
// Check reference is correct
if (!Arrays.equals(this.sender.getLastReference(), this.reference))
return ValidationResult.INVALID_REFERENCE;
// Check sender has enough funds
if (this.sender.getBalance(Asset.QORA, 1).compareTo(this.amount.add(this.fee)) == -1)
return ValidationResult.NO_BALANCE;
return ValidationResult.OK;
}
public void process(Connection connection) {
// TODO
public void process(Connection connection) throws SQLException {
this.save(connection);
// Update sender's balance
this.sender.setConfirmedBalance(connection, Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).subtract(this.amount).subtract(this.fee));
// Update recipient's balance
this.recipient.setConfirmedBalance(connection, Asset.QORA, this.recipient.getConfirmedBalance(Asset.QORA).add(this.amount));
// Update sender's reference
this.sender.setLastReference(connection, this.signature);
// If recipient has no reference yet, then this is their starting reference
if (this.recipient.getLastReference() == null)
this.recipient.setLastReference(connection, this.signature);
}
public void orphan(Connection connection) {
// TODO
public void orphan(Connection connection) throws SQLException {
this.delete(connection);
// Update sender's balance
this.sender.setConfirmedBalance(connection, Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).add(this.amount).add(this.fee));
// Update recipient's balance
this.recipient.setConfirmedBalance(connection, Asset.QORA, this.recipient.getConfirmedBalance(Asset.QORA).subtract(this.amount));
// Update sender's reference
this.sender.setLastReference(connection, this.reference);
/*
* If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own (which would have changed
* their last reference) thus this is their first reference so remove it.
*/
if (Arrays.equals(this.recipient.getLastReference(), this.signature))
this.recipient.setLastReference(connection, null);
}
}

View File

@ -25,6 +25,7 @@ import qora.block.BlockTransaction;
import settings.Settings;
import utils.Base58;
import utils.ParseException;
public abstract class Transaction {
@ -48,7 +49,7 @@ public abstract class Transaction {
// Validation results
public enum ValidationResult {
OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3);
OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6);
public final int value;
@ -201,7 +202,7 @@ public abstract class Transaction {
return blockChainHeight - ourHeight + 1;
}
// Load/Save
// Load/Save/Delete
// Typically called by sub-class' load-from-DB constructors
@ -217,7 +218,7 @@ public abstract class Transaction {
* @throws SQLException
*/
protected Transaction(TransactionType type, byte[] signature) throws SQLException {
ResultSet rs = DB.executeUsingBytes("SELECT reference, creator, creation, fee FROM Transactions WHERE signature = ?", signature);
ResultSet rs = DB.checkedExecute("SELECT reference, creator, creation, fee FROM Transactions WHERE signature = ? AND type = ?", signature, type.value);
if (rs == null)
throw new NoDataFoundException();
@ -238,6 +239,10 @@ public abstract class Transaction {
saveHelper.execute();
}
protected void delete(Connection connection) throws SQLException {
DB.checkedExecute("DELETE FROM Transactions WHERE signature = ?", this.signature);
}
// Navigation
/**
@ -285,12 +290,12 @@ public abstract class Transaction {
// Converters
public static Transaction parse(byte[] data) throws TransactionParseException {
public static Transaction parse(byte[] data) throws ParseException {
if (data == null)
return null;
if (data.length < TYPE_LENGTH)
throw new TransactionParseException("Byte data too short to determine transaction type");
throw new ParseException("Byte data too short to determine transaction type");
ByteBuffer byteBuffer = ByteBuffer.wrap(data);
@ -325,9 +330,11 @@ public abstract class Transaction {
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("signature", Base58.encode(this.signature));
json.put("confirmations", this.getConfirmations());
return json;
@ -363,10 +370,10 @@ public abstract class Transaction {
return this.creator.verify(this.signature, this.toBytesLessSignature());
}
public abstract ValidationResult isValid(Connection connection);
public abstract ValidationResult isValid(Connection connection) throws SQLException;
public abstract void process(Connection connection) throws SQLException;
public abstract void orphan(Connection connection);
public abstract void orphan(Connection connection) throws SQLException;
}

View File

@ -16,7 +16,7 @@ public class TransactionFactory {
* @throws SQLException
*/
public static Transaction fromSignature(byte[] signature) throws SQLException {
ResultSet resultSet = DB.executeUsingBytes("SELECT type, signature FROM Transactions WHERE signature = ?", signature);
ResultSet resultSet = DB.checkedExecute("SELECT type, signature FROM Transactions WHERE signature = ?", signature);
return fromResultSet(resultSet);
}
@ -28,7 +28,7 @@ public class TransactionFactory {
* @throws SQLException
*/
public static Transaction fromReference(byte[] reference) throws SQLException {
ResultSet resultSet = DB.executeUsingBytes("SELECT type, signature FROM Transactions WHERE reference = ?", reference);
ResultSet resultSet = DB.checkedExecute("SELECT type, signature FROM Transactions WHERE reference = ?", reference);
return fromResultSet(resultSet);
}

View File

@ -1,10 +0,0 @@
package qora.transaction;
@SuppressWarnings("serial")
public class TransactionParseException extends Exception {
public TransactionParseException(String message) {
super(message);
}
}

View File

@ -132,19 +132,19 @@ public class migrate extends common {
PreparedStatement issueAssetPStmt = c.prepareStatement("INSERT INTO IssueAssetTransactions "
+ formatWithPlaceholders("signature", "creator", "asset_name", "description", "quantity", "is_divisible"));
PreparedStatement transferAssetPStmt = c
.prepareStatement("INSERT INTO TransferAssetTransactions " + formatWithPlaceholders("signature", "sender", "recipient", "asset", "amount"));
.prepareStatement("INSERT INTO TransferAssetTransactions " + formatWithPlaceholders("signature", "sender", "recipient", "asset_id", "amount"));
PreparedStatement createAssetOrderPStmt = c.prepareStatement("INSERT INTO CreateAssetOrderTransactions "
+ formatWithPlaceholders("signature", "creator", "have_asset", "have_amount", "want_asset", "want_amount"));
+ formatWithPlaceholders("signature", "creator", "have_asset_id", "amount", "want_asset_id", "price"));
PreparedStatement cancelAssetOrderPStmt = c
.prepareStatement("INSERT INTO CancelAssetOrderTransactions " + formatWithPlaceholders("signature", "creator", "asset_order"));
PreparedStatement multiPaymentPStmt = c.prepareStatement("INSERT INTO MultiPaymentTransactions " + formatWithPlaceholders("signature", "sender"));
PreparedStatement deployATPStmt = c.prepareStatement("INSERT INTO DeployATTransactions "
+ formatWithPlaceholders("signature", "creator", "AT_name", "description", "AT_type", "AT_tags", "creation_bytes", "amount"));
PreparedStatement messagePStmt = c.prepareStatement("INSERT INTO MessageTransactions "
+ formatWithPlaceholders("signature", "sender", "recipient", "is_text", "is_encrypted", "amount", "asset", "data"));
+ formatWithPlaceholders("signature", "sender", "recipient", "is_text", "is_encrypted", "amount", "asset_id", "data"));
PreparedStatement sharedPaymentPStmt = c
.prepareStatement("INSERT INTO SharedTransactionPayments " + formatWithPlaceholders("signature", "recipient", "amount", "asset"));
.prepareStatement("INSERT INTO SharedTransactionPayments " + formatWithPlaceholders("signature", "recipient", "amount", "asset_id"));
PreparedStatement blockTxPStmt = c
.prepareStatement("INSERT INTO BlockTransactions " + formatWithPlaceholders("block_signature", "sequence", "transaction_signature"));

View File

@ -14,12 +14,12 @@ import qora.block.Block;
import qora.block.GenesisBlock;
import qora.transaction.GenesisTransaction;
import qora.transaction.Transaction;
import qora.transaction.TransactionParseException;
import utils.ParseException;
public class transactions extends common {
@Test
public void testGenesisSerialization() throws SQLException, TransactionParseException {
public void testGenesisSerialization() throws SQLException, ParseException {
GenesisBlock block = GenesisBlock.getInstance();
GenesisTransaction transaction = (GenesisTransaction) block.getTransactions().get(1);
@ -36,7 +36,7 @@ public class transactions extends common {
assertTrue(Arrays.equals(transaction.getSignature(), parsedTransaction.getSignature()));
}
public void testGenericSerialization(Transaction transaction) throws SQLException, TransactionParseException {
public void testGenericSerialization(Transaction transaction) throws SQLException, ParseException {
assertNotNull(transaction);
byte[] bytes = transaction.toBytes();
@ -47,7 +47,7 @@ public class transactions extends common {
}
@Test
public void testPaymentSerialization() throws SQLException, TransactionParseException {
public void testPaymentSerialization() throws SQLException, ParseException {
try (final Connection connection = DB.getConnection()) {
// Block 949 has lots of varied transactions
// Blocks 390 & 754 have only payment transactions

View File

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