From 4ce499c4443a5e1b6dcb4fb47d792001654007f6 Mon Sep 17 00:00:00 2001 From: catbref Date: Sun, 27 May 2018 14:59:30 +0100 Subject: [PATCH] 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. --- src/database/DB.java | 34 ++-- src/database/DatabaseUpdates.java | 16 +- src/qora/account/Account.java | 58 ++++-- src/qora/assets/Asset.java | 22 ++- src/qora/assets/Order.java | 141 +++++++++++++++ src/qora/assets/Trade.java | 47 +++++ src/qora/block/Block.java | 105 ++++++++++- src/qora/block/BlockChain.java | 2 +- src/qora/block/BlockTransaction.java | 14 +- .../transaction/CreateOrderTransaction.java | 165 ++++++++++++++++++ src/qora/transaction/GenesisTransaction.java | 30 +++- src/qora/transaction/PaymentTransaction.java | 76 ++++++-- src/qora/transaction/Transaction.java | 23 ++- src/qora/transaction/TransactionFactory.java | 4 +- .../TransactionParseException.java | 10 -- src/test/migrate.java | 8 +- src/test/transactions.java | 8 +- src/utils/ParseException.java | 10 ++ 18 files changed, 658 insertions(+), 115 deletions(-) create mode 100644 src/qora/assets/Order.java create mode 100644 src/qora/assets/Trade.java create mode 100644 src/qora/transaction/CreateOrderTransaction.java delete mode 100644 src/qora/transaction/TransactionParseException.java create mode 100644 src/utils/ParseException.java 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); + } + +}