diff --git a/src/database/DB.java b/src/database/DB.java
index 39d118ac..acde3d21 100644
--- a/src/database/DB.java
+++ b/src/database/DB.java
@@ -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.
*
* Note: calls ResultSet.next() therefore returned ResultSet is already pointing to first row.
- *
- * 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);
}
}
diff --git a/src/database/DatabaseUpdates.java b/src/database/DatabaseUpdates.java
index e65ec7f2..ca726ce9 100644
--- a/src/database/DatabaseUpdates.java
+++ b/src/database/DatabaseUpdates.java
@@ -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:
diff --git a/src/qora/account/Account.java b/src/qora/account/Account.java
index 3bf5ccaa..521042aa 100644
--- a/src/qora/account/Account.java
+++ b/src/qora/account/Account.java
@@ -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();
}
}
diff --git a/src/qora/assets/Asset.java b/src/qora/assets/Asset.java
index f87d6b9a..91f521a4 100644
--- a/src/qora/assets/Asset.java
+++ b/src/qora/assets/Asset.java
@@ -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);
}
}
diff --git a/src/qora/assets/Order.java b/src/qora/assets/Order.java
new file mode 100644
index 00000000..c6b26b0f
--- /dev/null
+++ b/src/qora/assets/Order.java
@@ -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 {
+
+ // 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 getInitiatedTrades() {}
+
+ // TODO
+ // public boolean isConfirmed() {}
+
+ // Load/Save/Delete
+
+ // Navigation
+
+ // XXX is this getInitiatedTrades() above?
+ public List 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;
+ }
+ }
+
+}
diff --git a/src/qora/assets/Trade.java b/src/qora/assets/Trade.java
new file mode 100644
index 00000000..42401586
--- /dev/null
+++ b/src/qora/assets/Trade.java
@@ -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;
+ }
+
+}
diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java
index 65e05a8f..adde5a95 100644
--- a/src/qora/block/Block.java
+++ b/src/qora/block/Block.java
@@ -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();
- 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 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;
}
diff --git a/src/qora/block/BlockChain.java b/src/qora/block/BlockChain.java
index 9a921d57..28ed3127 100644
--- a/src/qora/block/BlockChain.java
+++ b/src/qora/block/BlockChain.java
@@ -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;
diff --git a/src/qora/block/BlockTransaction.java b/src/qora/block/BlockTransaction.java
index 318cd317..7ad09784 100644
--- a/src/qora/block/BlockTransaction.java
+++ b/src/qora/block/BlockTransaction.java
@@ -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();
diff --git a/src/qora/transaction/CreateOrderTransaction.java b/src/qora/transaction/CreateOrderTransaction.java
new file mode 100644
index 00000000..d4bc6ef4
--- /dev/null
+++ b/src/qora/transaction/CreateOrderTransaction.java
@@ -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
+ }
+
+}
diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java
index 0c03e058..29e939fa 100644
--- a/src/qora/transaction/GenesisTransaction.java
+++ b/src/qora/transaction/GenesisTransaction.java
@@ -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);
}
}
diff --git a/src/qora/transaction/PaymentTransaction.java b/src/qora/transaction/PaymentTransaction.java
index a38e40ee..6af92785 100644
--- a/src/qora/transaction/PaymentTransaction.java
+++ b/src/qora/transaction/PaymentTransaction.java
@@ -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);
}
}
diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java
index 02cf337f..95e574ee 100644
--- a/src/qora/transaction/Transaction.java
+++ b/src/qora/transaction/Transaction.java
@@ -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;
}
diff --git a/src/qora/transaction/TransactionFactory.java b/src/qora/transaction/TransactionFactory.java
index d2c0d5fb..6f917fca 100644
--- a/src/qora/transaction/TransactionFactory.java
+++ b/src/qora/transaction/TransactionFactory.java
@@ -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);
}
diff --git a/src/qora/transaction/TransactionParseException.java b/src/qora/transaction/TransactionParseException.java
deleted file mode 100644
index a0fd4a1a..00000000
--- a/src/qora/transaction/TransactionParseException.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package qora.transaction;
-
-@SuppressWarnings("serial")
-public class TransactionParseException extends Exception {
-
- public TransactionParseException(String message) {
- super(message);
- }
-
-}
diff --git a/src/test/migrate.java b/src/test/migrate.java
index 6af71b3e..4c89b341 100644
--- a/src/test/migrate.java
+++ b/src/test/migrate.java
@@ -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"));
diff --git a/src/test/transactions.java b/src/test/transactions.java
index be11d272..9b726ff7 100644
--- a/src/test/transactions.java
+++ b/src/test/transactions.java
@@ -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
diff --git a/src/utils/ParseException.java b/src/utils/ParseException.java
new file mode 100644
index 00000000..03c6bef3
--- /dev/null
+++ b/src/utils/ParseException.java
@@ -0,0 +1,10 @@
+package utils;
+
+@SuppressWarnings("serial")
+public class ParseException extends Exception {
+
+ public ParseException(String message) {
+ super(message);
+ }
+
+}