diff --git a/src/database/DB.java b/src/database/DB.java index 45861286..5e388284 100644 --- a/src/database/DB.java +++ b/src/database/DB.java @@ -189,19 +189,36 @@ public class DB { */ public static ResultSet checkedExecute(String sql, Object... objects) throws SQLException { try (final Connection connection = DB.getConnection()) { - PreparedStatement preparedStatement = connection.prepareStatement(sql); - 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]); - - return checkedExecute(preparedStatement); + return checkedExecute(connection, sql, objects); } } + /** + * Execute SQL using connection and return ResultSet with but added checking. + *
+ * Typically for use within an ongoing SQL Transaction. + *
+ * Note: calls ResultSet.next() therefore returned ResultSet is already pointing to first row. + * + * @param connection + * @param sql + * @param objects + * @return ResultSet, or null if there are no found rows + * @throws SQLException + */ + public static ResultSet checkedExecute(Connection connection, String sql, Object... objects) throws SQLException { + PreparedStatement preparedStatement = connection.prepareStatement(sql); + 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]); + + return checkedExecute(preparedStatement); + } + /** * Execute PreparedStatement and return ResultSet with but added checking. *
@@ -264,14 +281,39 @@ public class DB { */ public static boolean exists(String tableName, String whereClause, Object... objects) throws SQLException { try (final Connection connection = DB.getConnection()) { - PreparedStatement preparedStatement = connection - .prepareStatement("SELECT TRUE FROM " + tableName + " WHERE " + whereClause + " ORDER BY NULL LIMIT 1"); - ResultSet resultSet = DB.checkedExecute(preparedStatement); - if (resultSet == null) - return false; - - return true; + return exists(connection, tableName, whereClause, objects); } } + /** + * Efficiently query database, using connection, for existing of matching row. + *
+ * Typically for use within an ongoing SQL Transaction. + *
+ * {@code whereClause} is SQL "WHERE" clause containing "?" placeholders suitable for use with PreparedStatements. + *
+ * Example call: + *
+ * {@code Connection connection = DB.getConnection();}
+ * {@code String manufacturer = "Lamborghini";}
+ * {@code int maxMileage = 100_000;}
+ * {@code boolean isAvailable = DB.exists(connection, "Cars", "manufacturer = ? AND mileage <= ?", manufacturer, maxMileage);}
+ *
+ * @param connection
+ * @param tableName
+ * @param whereClause
+ * @param objects
+ * @return true if matching row found in database, false otherwise
+ * @throws SQLException
+ */
+ public static boolean exists(Connection connection, String tableName, String whereClause, Object... objects) throws SQLException {
+ PreparedStatement preparedStatement = connection
+ .prepareStatement("SELECT TRUE FROM " + tableName + " WHERE " + whereClause + " ORDER BY NULL LIMIT 1");
+ ResultSet resultSet = DB.checkedExecute(preparedStatement);
+ if (resultSet == null)
+ return false;
+
+ return true;
+ }
+
}
diff --git a/src/database/DatabaseUpdates.java b/src/database/DatabaseUpdates.java
index de69be03..202035d3 100644
--- a/src/database/DatabaseUpdates.java
+++ b/src/database/DatabaseUpdates.java
@@ -82,21 +82,21 @@ public class DatabaseUpdates {
stmt.execute("SET FILES SPACE TRUE");
stmt.execute("CREATE TABLE DatabaseInfo ( version INTEGER NOT NULL )");
stmt.execute("INSERT INTO DatabaseInfo VALUES ( 0 )");
- stmt.execute("CREATE DOMAIN BlockSignature AS VARBINARY(128)");
- stmt.execute("CREATE DOMAIN Signature AS VARBINARY(64)");
- stmt.execute("CREATE DOMAIN QoraAddress AS VARCHAR(36)");
- stmt.execute("CREATE DOMAIN QoraPublicKey AS VARBINARY(32)");
- stmt.execute("CREATE DOMAIN QoraAmount AS DECIMAL(19, 8)");
- stmt.execute("CREATE DOMAIN RegisteredName AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
- stmt.execute("CREATE DOMAIN NameData AS VARCHAR(4000)");
- stmt.execute("CREATE DOMAIN PollName AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
- stmt.execute("CREATE DOMAIN PollOption AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
- stmt.execute("CREATE DOMAIN DataHash AS VARCHAR(100)");
- stmt.execute("CREATE DOMAIN AssetID AS BIGINT");
- stmt.execute("CREATE DOMAIN AssetName AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
- stmt.execute("CREATE DOMAIN AssetOrderID AS VARCHAR(100)");
- stmt.execute("CREATE DOMAIN ATName AS VARCHAR(200) COLLATE SQL_TEXT_UCC");
- stmt.execute("CREATE DOMAIN ATType AS VARCHAR(200) COLLATE SQL_TEXT_UCC");
+ stmt.execute("CREATE TYPE BlockSignature AS VARBINARY(128)");
+ stmt.execute("CREATE TYPE Signature AS VARBINARY(64)");
+ stmt.execute("CREATE TYPE QoraAddress AS VARCHAR(36)");
+ stmt.execute("CREATE TYPE QoraPublicKey AS VARBINARY(32)");
+ stmt.execute("CREATE TYPE QoraAmount AS DECIMAL(19, 8)");
+ stmt.execute("CREATE TYPE RegisteredName AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
+ stmt.execute("CREATE TYPE NameData AS VARCHAR(4000)");
+ stmt.execute("CREATE TYPE PollName AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
+ stmt.execute("CREATE TYPE PollOption AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
+ stmt.execute("CREATE TYPE DataHash AS VARCHAR(100)");
+ stmt.execute("CREATE TYPE AssetID AS BIGINT");
+ stmt.execute("CREATE TYPE AssetName AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
+ stmt.execute("CREATE TYPE AssetOrderID AS VARCHAR(100)");
+ stmt.execute("CREATE TYPE ATName AS VARCHAR(200) COLLATE SQL_TEXT_UCC");
+ stmt.execute("CREATE TYPE ATType AS VARCHAR(200) COLLATE SQL_TEXT_UCC");
break;
case 1:
@@ -221,9 +221,10 @@ public class DatabaseUpdates {
case 14:
// Issue Asset Transactions
- stmt.execute("CREATE TABLE IssueAssetTransactions (signature Signature, creator QoraPublicKey NOT NULL, asset_name AssetName NOT NULL, "
- + "description VARCHAR(4000) NOT NULL, quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, "
- + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
+ stmt.execute(
+ "CREATE TABLE IssueAssetTransactions (signature Signature, issuer QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, asset_name AssetName NOT NULL, "
+ + "description VARCHAR(4000) NOT NULL, quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, asset_id AssetID, "
+ + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
// For the future: maybe convert quantity from BIGINT to QoraAmount, regardless of divisibility
break;
@@ -275,6 +276,7 @@ public class DatabaseUpdates {
stmt.execute(
"CREATE TABLE Assets (asset_id AssetID IDENTITY, owner QoraPublicKey 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)");
+ stmt.execute("CREATE INDEX AssetNameIndex on Assets (asset_name)");
break;
case 22:
diff --git a/src/qora/account/Account.java b/src/qora/account/Account.java
index b4e59387..7a37afc7 100644
--- a/src/qora/account/Account.java
+++ b/src/qora/account/Account.java
@@ -40,13 +40,15 @@ public class Account {
return null;
}
- public BigDecimal getUnconfirmedBalance(long assetId) {
- // TODO
- return null;
+ public BigDecimal getConfirmedBalance(long assetId) throws SQLException {
+ try (final Connection connection = DB.getConnection()) {
+ return getConfirmedBalance(connection, assetId);
+ }
}
- public BigDecimal getConfirmedBalance(long assetId) throws SQLException {
- ResultSet resultSet = DB.checkedExecute("SELECT balance FROM AccountBalances WHERE account = ? and asset_id = ?", this.getAddress(), assetId);
+ public BigDecimal getConfirmedBalance(Connection connection, long assetId) throws SQLException {
+ ResultSet resultSet = DB.checkedExecute(connection, "SELECT balance FROM AccountBalances WHERE account = ? and asset_id = ?", this.getAddress(),
+ assetId);
if (resultSet == null)
return BigDecimal.ZERO.setScale(8);
@@ -59,6 +61,10 @@ public class Account {
saveHelper.execute();
}
+ public void deleteBalance(Connection connection, long assetId) throws SQLException {
+ DB.checkedExecute(connection, "DELETE FROM AccountBalances WHERE account = ? and asset_id = ?", this.getAddress(), assetId);
+ }
+
// Reference manipulations
/**
@@ -68,7 +74,22 @@ public class Account {
* @throws SQLException
*/
public byte[] getLastReference() throws SQLException {
- ResultSet resultSet = DB.checkedExecute("SELECT reference FROM Accounts WHERE account = ?", this.getAddress());
+ try (final Connection connection = DB.getConnection()) {
+ return getLastReference(connection);
+ }
+ }
+
+ /**
+ * Fetch last reference for account using supplied DB connection.
+ *
+ * Typically for use within an ongoing SQL Transaction. + * + * @param connection + * @return byte[] reference, or null if no reference or account not found. + * @throws SQLException + */ + public byte[] getLastReference(Connection connection) throws SQLException { + ResultSet resultSet = DB.checkedExecute(connection, "SELECT reference FROM Accounts WHERE account = ?", this.getAddress()); if (resultSet == null) return null; diff --git a/src/qora/account/GenesisAccount.java b/src/qora/account/GenesisAccount.java index 7c12b05f..170983d6 100644 --- a/src/qora/account/GenesisAccount.java +++ b/src/qora/account/GenesisAccount.java @@ -1,11 +1,9 @@ package qora.account; -import com.google.common.primitives.Bytes; - public final class GenesisAccount extends PublicKeyAccount { public GenesisAccount() { - super(Bytes.ensureCapacity(new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 }, 32, 0)); + super(new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 }); } } diff --git a/src/qora/assets/Asset.java b/src/qora/assets/Asset.java index 82d0f522..808455cc 100644 --- a/src/qora/assets/Asset.java +++ b/src/qora/assets/Asset.java @@ -7,7 +7,7 @@ import java.sql.SQLException; import database.DB; import database.NoDataFoundException; import database.SaveHelper; -import qora.account.PublicKeyAccount; +import qora.account.Account; import qora.transaction.Transaction; /* @@ -24,20 +24,17 @@ public class Asset { // Properties private Long assetId; - private PublicKeyAccount owner; + private Account owner; private String name; private String description; private long quantity; private boolean isDivisible; private byte[] reference; - // Property lengths - private static final int OWNER_LENGTH = Transaction.CREATOR_LENGTH; - // 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) { + public Asset(Long assetId, String owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) { this.assetId = assetId; - this.owner = owner; + this.owner = new Account(owner); this.name = name; this.description = description; this.quantity = quantity; @@ -46,10 +43,40 @@ public class Asset { } // New asset with unassigned assetId - public Asset(PublicKeyAccount owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) { + public Asset(String owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) { this(null, owner, name, description, quantity, isDivisible, reference); } + // Getters/Setters + + public Long getAssetId() { + return this.assetId; + } + + public Account getOwner() { + return this.owner; + } + + public String getName() { + return this.name; + } + + public String getDescription() { + return this.description; + } + + public long getQuantity() { + return this.quantity; + } + + public boolean isDivisible() { + return this.isDivisible; + } + + public byte[] getReference() { + return this.reference; + } + // Load/Save/Delete/Exists protected Asset(long assetId) throws SQLException { @@ -60,7 +87,7 @@ public class Asset { if (rs == null) throw new NoDataFoundException(); - this.owner = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(1), OWNER_LENGTH)); + this.owner = new Account(rs.getString(1)); this.name = rs.getString(2); this.description = rs.getString(3); this.quantity = rs.getLong(4); @@ -86,8 +113,24 @@ public class Asset { this.assetId = DB.callIdentity(connection); } + public void delete(Connection connection) throws SQLException { + DB.checkedExecute(connection, "DELETE FROM Assets WHERE asset_id = ?", this.assetId); + } + public static boolean exists(long assetId) throws SQLException { return DB.exists("Assets", "asset_id = ?", assetId); } + public static boolean exists(Connection connection, long assetId) throws SQLException { + return DB.exists(connection, "Assets", "asset_id = ?", assetId); + } + + public static boolean exists(String assetName) throws SQLException { + return DB.exists("Assets", "asset_name = ?", assetName); + } + + public static boolean exists(Connection connection, String assetName) throws SQLException { + return DB.exists(connection, "Assets", "asset_name = ?", assetName); + } + } diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index 352c94f6..3d0f3aa8 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -120,6 +120,7 @@ public class Block { public static final int MESSAGE_RELEASE_HEIGHT = 99000; public static final int AT_BLOCK_HEIGHT_RELEASE = 99000; public static final long POWFIX_RELEASE_TIMESTAMP = 1456426800000L; // Block Version 3 // 2016-02-25T19:00:00+00:00 + public static final long ASSETS_RELEASE_TIMESTAMP = 0L; // From Qora epoch // Constructors @@ -741,10 +742,11 @@ public class Block { } /** - * Returns whether Block is valid. Expected to be called within SQL Transaction. + * Returns whether Block is valid using passed connection. *
- * Performs various tests like checking for parent block, correct block timestamp, version, generating balance, etc.
- * Also checks block's transactions using an HSQLDB "SAVEPOINT" and hence needs to be called within an ongoing SQL Transaction.
+ * Performs various tests like checking for parent block, correct block timestamp, version, generating balance, etc.
+ *
+ * Checks block's transactions using an HSQLDB "SAVEPOINT" and hence needs to be called within an ongoing SQL Transaction. * * @param connection * @return true if block is valid, false otherwise. @@ -795,9 +797,6 @@ public class Block { // Check transactions DB.createSavepoint(connection, "BLOCK_TRANSACTIONS"); - // XXX: we might need to catch SQLExceptions and not rollback which could cause a new exception? - // OR: catch, attempt to rollback and then re-throw caught exception? - // OR: don't catch, attempt to rollback, catch exception during rollback then return false? try { for (Transaction transaction : this.getTransactions()) { // GenesisTransactions are not allowed (GenesisBlock overrides isValid() to allow them) @@ -823,7 +822,14 @@ public class Block { } } finally { // Revert back to savepoint - DB.rollbackToSavepoint(connection, "BLOCK_TRANSACTIONS"); + try { + DB.rollbackToSavepoint(connection, "BLOCK_TRANSACTIONS"); + } catch (SQLException e) { + /* + * Rollback failure most likely due to prior SQLException, so catch rollback's SQLException and discard. A "return false" in try-block will + * still return false, prior SQLException propagates to caller and successful completion of try-block continues on after rollback. + */ + } } // Block is valid diff --git a/src/qora/block/BlockChain.java b/src/qora/block/BlockChain.java index 5afe5310..eb1eb365 100644 --- a/src/qora/block/BlockChain.java +++ b/src/qora/block/BlockChain.java @@ -65,8 +65,8 @@ public class BlockChain { // Add QORA asset. // NOTE: Asset's transaction reference is Genesis Block's generator signature which doesn't exist as a transaction! - Asset qoraAsset = new Asset(Asset.QORA, genesisBlock.getGenerator(), "Qora", "This is the simulated Qora asset.", 10_000_000_000L, true, - genesisBlock.getGeneratorSignature()); + Asset qoraAsset = new Asset(Asset.QORA, genesisBlock.getGenerator().getAddress(), "Qora", "This is the simulated Qora asset.", 10_000_000_000L, + true, genesisBlock.getGeneratorSignature()); qoraAsset.save(connection); } } diff --git a/src/qora/block/BlockTransaction.java b/src/qora/block/BlockTransaction.java index 7ad09784..b9f737ce 100644 --- a/src/qora/block/BlockTransaction.java +++ b/src/qora/block/BlockTransaction.java @@ -4,8 +4,6 @@ import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; -import org.json.simple.JSONObject; - import database.DB; import database.NoDataFoundException; import database.SaveHelper; @@ -127,13 +125,4 @@ public class BlockTransaction { return TransactionFactory.fromSignature(this.transactionSignature); } - // Converters - - public JSONObject toJSON() { - // TODO - return null; - } - - // Processing - } diff --git a/src/qora/block/GenesisBlock.java b/src/qora/block/GenesisBlock.java index e359b900..a5e5769d 100644 --- a/src/qora/block/GenesisBlock.java +++ b/src/qora/block/GenesisBlock.java @@ -290,20 +290,25 @@ public class GenesisBlock extends Block { // Passing expected size to ByteArrayOutputStream avoids reallocation when adding more bytes than default 32. // See below for explanation of some of the values used to calculated expected size. ByteArrayOutputStream bytes = new ByteArrayOutputStream(8 + 64 + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH); + /* * NOTE: Historic code had genesis block using Longs.toByteArray() compared to standard block's Ints.toByteArray. The subsequent * Bytes.ensureCapacity(versionBytes, 0, 4) did not truncate versionBytes back to 4 bytes either. This means 8 bytes were used even though * VERSION_LENGTH is set to 4. Correcting this historic bug will break genesis block signatures! */ bytes.write(Longs.toByteArray(GENESIS_BLOCK_VERSION)); + /* * NOTE: Historic code had the reference expanded to only 64 bytes whereas standard block references are 128 bytes. Correcting this historic bug * will break genesis block signatures! */ bytes.write(Bytes.ensureCapacity(GENESIS_REFERENCE, 64, 0)); + bytes.write(Longs.toByteArray(GENESIS_GENERATING_BALANCE.longValue())); - // NOTE: Genesis account's public key is only 8 bytes, not the usual 32. - bytes.write(GENESIS_GENERATOR.getPublicKey()); + + // NOTE: Genesis account's public key is only 8 bytes, not the usual 32, so we have to pad. + bytes.write(Bytes.ensureCapacity(GENESIS_GENERATOR.getPublicKey(), 32, 0)); + return bytes.toByteArray(); } catch (IOException e) { throw new RuntimeException(e); diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java index 29e939fa..2f8fed3b 100644 --- a/src/qora/transaction/GenesisTransaction.java +++ b/src/qora/transaction/GenesisTransaction.java @@ -221,8 +221,8 @@ public class GenesisTransaction extends Transaction { public void orphan(Connection connection) throws SQLException { this.delete(connection); - // Set recipient's balance - this.recipient.setConfirmedBalance(connection, Asset.QORA, BigDecimal.ZERO); + // Reset recipient's balance + this.recipient.deleteBalance(connection, Asset.QORA); // Set recipient's reference recipient.setLastReference(connection, null); diff --git a/src/qora/transaction/IssueAssetTransaction.java b/src/qora/transaction/IssueAssetTransaction.java new file mode 100644 index 00000000..f15baa34 --- /dev/null +++ b/src/qora/transaction/IssueAssetTransaction.java @@ -0,0 +1,351 @@ +package qora.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +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; + +import com.google.common.hash.HashCode; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +import database.DB; +import database.NoDataFoundException; +import database.SaveHelper; +import qora.account.Account; +import qora.account.PublicKeyAccount; +import qora.assets.Asset; +import qora.block.Block; +import qora.crypto.Crypto; +import utils.Base58; +import utils.NTP; +import utils.ParseException; +import utils.Serialization; + +public class IssueAssetTransaction extends Transaction { + + // Properties + private PublicKeyAccount issuer; + private Account owner; + private String assetName; + private String description; + private long quantity; + private boolean isDivisible; + // assetId assigned during save() or during load from database + private Long assetId = null; + + // Property lengths + private static final int ISSUER_LENGTH = CREATOR_LENGTH; + private static final int OWNER_LENGTH = RECIPIENT_LENGTH; + private static final int NAME_SIZE_LENGTH = 4; + private static final int DESCRIPTION_SIZE_LENGTH = 4; + private static final int QUANTITY_LENGTH = 8; + private static final int IS_DIVISIBLE_LENGTH = 1; + private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + ISSUER_LENGTH + OWNER_LENGTH + NAME_SIZE_LENGTH + DESCRIPTION_SIZE_LENGTH + + QUANTITY_LENGTH + IS_DIVISIBLE_LENGTH; + + // Other useful lengths + private static final int MAX_NAME_SIZE = 400; + private static final int MAX_DESCRIPTION_SIZE = 4000; + + // Constructors + + /** + * Reconstruct an IssueAssetTransaction, including signature. + * + * @param issuer + * @param owner + * @param assetName + * @param description + * @param quantity + * @param isDivisible + * @param fee + * @param timestamp + * @param reference + * @param signature + */ + public IssueAssetTransaction(PublicKeyAccount issuer, String owner, String assetName, String description, long quantity, boolean isDivisible, + BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + super(TransactionType.ISSUE_ASSET, fee, issuer, timestamp, reference, signature); + + this.issuer = issuer; + this.owner = new Account(owner); + this.assetName = assetName; + this.description = description; + this.quantity = quantity; + this.isDivisible = isDivisible; + } + + /** + * Construct a new IssueAssetTransaction. + * + * @param issuer + * @param owner + * @param assetName + * @param description + * @param quantity + * @param isDivisible + * @param fee + * @param timestamp + * @param reference + */ + public IssueAssetTransaction(PublicKeyAccount issuer, String owner, String assetName, String description, long quantity, boolean isDivisible, + BigDecimal fee, long timestamp, byte[] reference) { + this(issuer, owner, assetName, description, quantity, isDivisible, fee, timestamp, reference, null); + } + + // Getters/Setters + + public PublicKeyAccount getIssuer() { + return this.issuer; + } + + public Account getOwner() { + return this.owner; + } + + public String getAssetName() { + return this.assetName; + } + + public String getDescription() { + return this.description; + } + + public long getQuantity() { + return this.quantity; + } + + public boolean isDivisible() { + return this.isDivisible; + } + + // More information + + /** + * Return asset ID assigned if this transaction has been processed. + * + * @return asset ID if transaction has been processed and asset created, null otherwise + */ + public Long getAssetId() { + return this.assetId; + } + + public int getDataLength() { + return TYPE_LENGTH + TYPELESS_LENGTH + assetName.length() + description.length(); + } + + // Load/Save + + /** + * Construct IssueAssetTransaction from DB using signature. + * + * @param signature + * @throws NoDataFoundException + * if no matching row found + * @throws SQLException + */ + protected IssueAssetTransaction(byte[] signature) throws SQLException { + super(TransactionType.ISSUE_ASSET, signature); + + ResultSet rs = DB.checkedExecute( + "SELECT issuer, owner, asset_name, description, quantity, is_divisible, asset_id FROM IssueAssetTransactions WHERE signature = ?", signature); + if (rs == null) + throw new NoDataFoundException(); + + this.issuer = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(2), ISSUER_LENGTH)); + this.owner = new Account(rs.getString(2)); + this.assetName = rs.getString(3); + this.description = rs.getString(4); + this.quantity = rs.getLong(5); + this.isDivisible = rs.getBoolean(6); + this.assetId = rs.getLong(7); + } + + /** + * Load IssueAssetTransaction from DB using signature. + * + * @param signature + * @return PaymentTransaction, or null if not found + * @throws SQLException + */ + public static IssueAssetTransaction fromSignature(byte[] signature) throws SQLException { + try { + return new IssueAssetTransaction(signature); + } catch (NoDataFoundException e) { + return null; + } + } + + @Override + public void save(Connection connection) throws SQLException { + super.save(connection); + + SaveHelper saveHelper = new SaveHelper(connection, "IssueAssetTransactions"); + saveHelper.bind("signature", this.signature).bind("creator", this.creator.getPublicKey()).bind("asset_name", this.assetName) + .bind("description", this.description).bind("quantity", this.quantity).bind("is_divisible", this.isDivisible).bind("asset_id", this.assetId); + saveHelper.execute(); + } + + // Converters + + protected static Transaction parse(ByteBuffer byteBuffer) throws ParseException { + if (byteBuffer.remaining() < TYPELESS_LENGTH) + throw new ParseException("Byte data too short for IssueAssetTransaction"); + + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + PublicKeyAccount issuer = Serialization.deserializePublicKey(byteBuffer); + String owner = Serialization.deserializeRecipient(byteBuffer); + + String assetName = Serialization.deserializeSizedString(byteBuffer, MAX_NAME_SIZE); + String description = Serialization.deserializeSizedString(byteBuffer, MAX_DESCRIPTION_SIZE); + + // Still need to make sure there are enough bytes left for remaining fields + if (byteBuffer.remaining() < QUANTITY_LENGTH + IS_DIVISIBLE_LENGTH + SIGNATURE_LENGTH) + throw new ParseException("Byte data too short for IssueAssetTransaction"); + + long quantity = byteBuffer.getLong(); + boolean isDivisible = byteBuffer.get() != 0; + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new IssueAssetTransaction(issuer, owner, assetName, description, quantity, isDivisible, fee, timestamp, reference, signature); + } + + @SuppressWarnings("unchecked") + @Override + public JSONObject toJSON() throws SQLException { + JSONObject json = getBaseJSON(); + + json.put("issuer", this.creator.getAddress()); + json.put("issuerPublicKey", HashCode.fromBytes(this.creator.getPublicKey()).toString()); + json.put("owner", this.owner.getAddress()); + json.put("assetName", this.assetName); + json.put("description", this.description); + json.put("quantity", this.quantity); + json.put("isDivisible", this.isDivisible); + + 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); + bytes.write(this.issuer.getPublicKey()); + bytes.write(Base58.decode(this.owner.getAddress())); + + bytes.write(Ints.toByteArray(this.assetName.length())); + bytes.write(this.assetName.getBytes("UTF-8")); + + bytes.write(Ints.toByteArray(this.description.length())); + bytes.write(this.description.getBytes("UTF-8")); + + bytes.write(Longs.toByteArray(this.quantity)); + bytes.write((byte) (this.isDivisible ? 1 : 0)); + + bytes.write(this.signature); + return bytes.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // Processing + + public ValidationResult isValid(Connection connection) throws SQLException { + // Lowest cost checks first + + // Are IssueAssetTransactions even allowed at this point? + if (NTP.getTime() < Block.ASSETS_RELEASE_TIMESTAMP) + return ValidationResult.NOT_YET_RELEASED; + + // Check owner address is valid + if (!Crypto.isValidAddress(this.owner.getAddress())) + return ValidationResult.INVALID_ADDRESS; + + // Check name size bounds + if (this.assetName.length() < 1 || this.assetName.length() > MAX_NAME_SIZE) + return ValidationResult.INVALID_NAME_LENGTH; + + // Check description size bounds + if (this.description.length() < 1 || this.description.length() > MAX_NAME_SIZE) + return ValidationResult.INVALID_DESCRIPTION_LENGTH; + + // Check quantity - either 10 billion or if that's not enough: a billion billion! + long maxQuantity = this.isDivisible ? 10_000_000_000L : 1_000_000_000_000_000_000L; + if (this.quantity < 1 || this.quantity > maxQuantity) + return ValidationResult.INVALID_QUANTITY; + + // Check fee is positive + if (this.fee.compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + // Check reference is correct + if (!Arrays.equals(this.issuer.getLastReference(connection), this.reference)) + return ValidationResult.INVALID_REFERENCE; + + // Check issuer has enough funds + if (this.issuer.getConfirmedBalance(connection, Asset.QORA).compareTo(this.fee) == -1) + return ValidationResult.NO_BALANCE; + + // XXX: Surely we want to check the asset name isn't already taken? + if (Asset.exists(connection, this.assetName)) + return ValidationResult.ASSET_ALREADY_EXISTS; + + return ValidationResult.OK; + } + + public void process(Connection connection) throws SQLException { + // Issue asset + Asset asset = new Asset(owner.getAddress(), this.assetName, this.description, this.quantity, this.isDivisible, this.reference); + asset.save(connection); + + // Note newly assigned asset ID in our transaction record + this.assetId = asset.getAssetId(); + + this.save(connection); + + // Update issuer's balance + this.issuer.setConfirmedBalance(connection, Asset.QORA, this.issuer.getConfirmedBalance(connection, Asset.QORA).subtract(this.fee)); + + // Update issuer's reference + this.issuer.setLastReference(connection, this.signature); + + // Add asset to owner + this.owner.setConfirmedBalance(connection, this.assetId, BigDecimal.valueOf(this.quantity).setScale(8)); + } + + public void orphan(Connection connection) throws SQLException { + // Remove asset from owner + this.owner.deleteBalance(connection, this.assetId); + + // Unissue asset + Asset asset = Asset.fromAssetId(this.assetId); + asset.delete(connection); + + this.delete(connection); + + // Update issuer's balance + this.issuer.setConfirmedBalance(connection, Asset.QORA, this.issuer.getConfirmedBalance(connection, Asset.QORA).add(this.fee)); + + // Update issuer's reference + this.issuer.setLastReference(connection, this.reference); + } + +} diff --git a/src/qora/transaction/MessageTransaction.java b/src/qora/transaction/MessageTransaction.java index 57350e80..61c0d2a6 100644 --- a/src/qora/transaction/MessageTransaction.java +++ b/src/qora/transaction/MessageTransaction.java @@ -122,7 +122,7 @@ public class MessageTransaction extends Transaction { // Load/Save /** - * Load MessageTransaction from DB using signature. + * Construct MessageTransaction from DB using signature. * * @param signature * @throws NoDataFoundException @@ -148,7 +148,7 @@ public class MessageTransaction extends Transaction { } /** - * Load MessageTransaction from DB using signature + * Load MessageTransaction from DB using signature. * * @param signature * @return MessageTransaction, or null if not found @@ -190,6 +190,7 @@ public class MessageTransaction extends Transaction { byte[] reference = new byte[REFERENCE_LENGTH]; byteBuffer.get(reference); + PublicKeyAccount sender = Serialization.deserializePublicKey(byteBuffer); String recipient = Serialization.deserializeRecipient(byteBuffer); @@ -213,6 +214,7 @@ public class MessageTransaction extends Transaction { boolean isText = byteBuffer.get() != 0; BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + byte[] signature = new byte[SIGNATURE_LENGTH]; byteBuffer.get(signature); @@ -305,25 +307,25 @@ public class MessageTransaction extends Transaction { return ValidationResult.NEGATIVE_FEE; // Check reference is correct - if (!Arrays.equals(this.sender.getLastReference(), this.reference)) + if (!Arrays.equals(this.sender.getLastReference(connection), this.reference)) return ValidationResult.INVALID_REFERENCE; // Does asset exist? (This test not present in gen1) - if (this.assetId != Asset.QORA && !Asset.exists(this.assetId)) + if (this.assetId != Asset.QORA && !Asset.exists(connection, this.assetId)) return ValidationResult.ASSET_DOES_NOT_EXIST; // If asset is QORA then we need to check amount + fee in one go if (this.assetId == Asset.QORA) { // Check sender has enough funds for amount + fee in QORA - if (this.sender.getBalance(Asset.QORA, 1).compareTo(this.amount.add(this.fee)) == -1) + if (this.sender.getConfirmedBalance(connection, Asset.QORA).compareTo(this.amount.add(this.fee)) == -1) return ValidationResult.NO_BALANCE; } else { // Check sender has enough funds for amount in whatever asset - if (this.sender.getBalance(this.assetId, 1).compareTo(this.amount) == -1) + if (this.sender.getConfirmedBalance(connection, this.assetId).compareTo(this.amount) == -1) return ValidationResult.NO_BALANCE; // Check sender has enough funds for fee in QORA - if (this.sender.getBalance(Asset.QORA, 1).compareTo(this.fee) == -1) + if (this.sender.getConfirmedBalance(connection, Asset.QORA).compareTo(this.fee) == -1) return ValidationResult.NO_BALANCE; } @@ -334,18 +336,18 @@ public class MessageTransaction extends Transaction { this.save(connection); // Update sender's balance due to amount - this.sender.setConfirmedBalance(connection, this.assetId, this.sender.getConfirmedBalance(this.assetId).subtract(this.amount)); + this.sender.setConfirmedBalance(connection, this.assetId, this.sender.getConfirmedBalance(connection, this.assetId).subtract(this.amount)); // Update sender's balance due to fee - this.sender.setConfirmedBalance(connection, Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).subtract(this.fee)); + this.sender.setConfirmedBalance(connection, Asset.QORA, this.sender.getConfirmedBalance(connection, Asset.QORA).subtract(this.fee)); // Update recipient's balance - this.recipient.setConfirmedBalance(connection, this.assetId, this.recipient.getConfirmedBalance(this.assetId).add(this.amount)); + this.recipient.setConfirmedBalance(connection, this.assetId, this.recipient.getConfirmedBalance(connection, this.assetId).add(this.amount)); // Update sender's reference this.sender.setLastReference(connection, this.signature); // For QORA amounts only: if recipient has no reference yet, then this is their starting reference - if (this.assetId == Asset.QORA && this.recipient.getLastReference() == null) + if (this.assetId == Asset.QORA && this.recipient.getLastReference(connection) == null) this.recipient.setLastReference(connection, this.signature); } @@ -353,12 +355,12 @@ public class MessageTransaction extends Transaction { this.delete(connection); // Update sender's balance due to amount - this.sender.setConfirmedBalance(connection, this.assetId, this.sender.getConfirmedBalance(this.assetId).add(this.amount)); + this.sender.setConfirmedBalance(connection, this.assetId, this.sender.getConfirmedBalance(connection, this.assetId).add(this.amount)); // Update sender's balance due to fee - this.sender.setConfirmedBalance(connection, Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).add(this.fee)); + this.sender.setConfirmedBalance(connection, Asset.QORA, this.sender.getConfirmedBalance(connection, Asset.QORA).add(this.fee)); // Update recipient's balance - this.recipient.setConfirmedBalance(connection, this.assetId, this.recipient.getConfirmedBalance(this.assetId).subtract(this.amount)); + this.recipient.setConfirmedBalance(connection, this.assetId, this.recipient.getConfirmedBalance(connection, this.assetId).subtract(this.amount)); // Update sender's reference this.sender.setLastReference(connection, this.reference); @@ -367,7 +369,7 @@ public class MessageTransaction extends Transaction { * For QORA amounts only: 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 (this.assetId == Asset.QORA && Arrays.equals(this.recipient.getLastReference(), this.signature)) + if (this.assetId == Asset.QORA && Arrays.equals(this.recipient.getLastReference(connection), this.signature)) this.recipient.setLastReference(connection, null); } diff --git a/src/qora/transaction/PaymentTransaction.java b/src/qora/transaction/PaymentTransaction.java index 6af92785..a16ec76d 100644 --- a/src/qora/transaction/PaymentTransaction.java +++ b/src/qora/transaction/PaymentTransaction.java @@ -76,7 +76,7 @@ public class PaymentTransaction extends Transaction { // Load/Save /** - * Load PaymentTransaction from DB using signature. + * Construct PaymentTransaction from DB using signature. * * @param signature * @throws NoDataFoundException @@ -96,7 +96,7 @@ public class PaymentTransaction extends Transaction { } /** - * Load PaymentTransaction from DB using signature + * Load PaymentTransaction from DB using signature. * * @param signature * @return PaymentTransaction, or null if not found @@ -127,12 +127,15 @@ public class PaymentTransaction extends Transaction { throw new ParseException("Byte data too short for PaymentTransaction"); long timestamp = byteBuffer.getLong(); + byte[] reference = new byte[REFERENCE_LENGTH]; byteBuffer.get(reference); + PublicKeyAccount sender = Serialization.deserializePublicKey(byteBuffer); String recipient = Serialization.deserializeRecipient(byteBuffer); BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + byte[] signature = new byte[SIGNATURE_LENGTH]; byteBuffer.get(signature); @@ -172,7 +175,7 @@ public class PaymentTransaction extends Transaction { // Processing public ValidationResult isValid(Connection connection) throws SQLException { - // Non-database checks first + // Lowest cost checks first // Check recipient is a valid address if (!Crypto.isValidAddress(this.recipient.getAddress())) @@ -187,11 +190,11 @@ public class PaymentTransaction extends Transaction { return ValidationResult.NEGATIVE_FEE; // Check reference is correct - if (!Arrays.equals(this.sender.getLastReference(), this.reference)) + if (!Arrays.equals(this.sender.getLastReference(connection), 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) + if (this.sender.getConfirmedBalance(connection, Asset.QORA).compareTo(this.amount.add(this.fee)) == -1) return ValidationResult.NO_BALANCE; return ValidationResult.OK; @@ -201,16 +204,16 @@ public class PaymentTransaction extends Transaction { this.save(connection); // Update sender's balance - this.sender.setConfirmedBalance(connection, Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).subtract(this.amount).subtract(this.fee)); + this.sender.setConfirmedBalance(connection, Asset.QORA, this.sender.getConfirmedBalance(connection, 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)); + this.recipient.setConfirmedBalance(connection, Asset.QORA, this.recipient.getConfirmedBalance(connection, 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) + if (this.recipient.getLastReference(connection) == null) this.recipient.setLastReference(connection, this.signature); } @@ -218,10 +221,10 @@ public class PaymentTransaction extends Transaction { this.delete(connection); // Update sender's balance - this.sender.setConfirmedBalance(connection, Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).add(this.amount).add(this.fee)); + this.sender.setConfirmedBalance(connection, Asset.QORA, this.sender.getConfirmedBalance(connection, 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)); + this.recipient.setConfirmedBalance(connection, Asset.QORA, this.recipient.getConfirmedBalance(connection, Asset.QORA).subtract(this.amount)); // Update sender's reference this.sender.setLastReference(connection, this.reference); @@ -230,7 +233,7 @@ public class PaymentTransaction extends Transaction { * 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)) + if (Arrays.equals(this.recipient.getLastReference(connection), this.signature)) this.recipient.setLastReference(connection, null); } diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index 8bcc58d7..c90fd6fd 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -49,8 +49,8 @@ public abstract class Transaction { // Validation results public enum ValidationResult { - OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6), INVALID_DATA_LENGTH(27), ASSET_DOES_NOT_EXIST( - 29), NOT_YET_RELEASED(1000); + OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6), INVALID_NAME_LENGTH(7), INVALID_DESCRIPTION_LENGTH( + 18), INVALID_DATA_LENGTH(27), INVALID_QUANTITY(28), ASSET_DOES_NOT_EXIST(29), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000); public final int value; @@ -258,7 +258,7 @@ public abstract class Transaction { protected void delete(Connection connection) throws SQLException { // NOTE: The corresponding row in sub-table is deleted automatically by the database thanks to "ON DELETE CASCADE" in the sub-table's FOREIGN KEY // definition. - DB.checkedExecute("DELETE FROM Transactions WHERE signature = ?", this.signature); + DB.checkedExecute(connection, "DELETE FROM Transactions WHERE signature = ?", this.signature); } // Navigation diff --git a/src/test/exceptions.java b/src/test/exceptions.java new file mode 100644 index 00000000..8a8ac0e6 --- /dev/null +++ b/src/test/exceptions.java @@ -0,0 +1,90 @@ +package test; + +import static org.junit.Assert.*; + +import org.junit.Test; +import qora.block.Block; + +public class exceptions { + + /** + * Proof of concept for block processing throwing transaction-related SQLException rather than savepoint-rollback-related SQLException. + *
+ * See {@link Block#isValid(Connection)}. + */ + @Test + public void testBlockProcessingExceptions() { + try { + simulateThrow(); + fail("Should not return result"); + } catch (Exception e) { + assertEquals("Transaction issue", e.getMessage()); + } + + try { + boolean result = simulateFalse(); + assertFalse(result); + } catch (Exception e) { + fail("Unexpected exception: " + e.getMessage()); + } + + try { + boolean result = simulateTrue(); + assertTrue(result); + } catch (Exception e) { + fail("Unexpected exception: " + e.getMessage()); + } + + } + + public boolean simulateThrow() throws Exception { + // simulate create savepoint (no-op) + + try { + // simulate processing transactions but an exception is thrown + throw new Exception("Transaction issue"); + } finally { + // attempt to rollback + try { + // simulate failing to rollback due to prior exception + throw new Exception("Rollback issue"); + } catch (Exception e) { + // test discard of rollback exception, leaving prior exception + } + } + } + + public boolean simulateFalse() throws Exception { + // simulate create savepoint (no-op) + + try { + // simulate processing transactions but false returned + return false; + } finally { + // attempt to rollback + try { + // simulate successful rollback (no-op) + } catch (Exception e) { + // test discard of rollback exception, leaving prior exception + } + } + } + + public boolean simulateTrue() throws Exception { + // simulate create savepoint (no-op) + + try { + // simulate processing transactions successfully + } finally { + // attempt to rollback + try { + // simulate successful rollback (no-op) + } catch (Exception e) { + // test discard of rollback exception, leaving prior exception + } + } + + return true; + } + +} diff --git a/src/test/migrate.java b/src/test/migrate.java index 2f4bbf11..76bfdcb0 100644 --- a/src/test/migrate.java +++ b/src/test/migrate.java @@ -131,7 +131,7 @@ public class migrate extends common { PreparedStatement arbitraryPStmt = c .prepareStatement("INSERT INTO ArbitraryTransactions " + formatWithPlaceholders("signature", "creator", "service", "data_hash")); PreparedStatement issueAssetPStmt = c.prepareStatement("INSERT INTO IssueAssetTransactions " - + formatWithPlaceholders("signature", "creator", "asset_name", "description", "quantity", "is_divisible")); + + formatWithPlaceholders("signature", "issuer", "owner", "asset_name", "description", "quantity", "is_divisible")); PreparedStatement transferAssetPStmt = c .prepareStatement("INSERT INTO TransferAssetTransactions " + formatWithPlaceholders("signature", "sender", "recipient", "asset_id", "amount")); PreparedStatement createAssetOrderPStmt = c.prepareStatement("INSERT INTO CreateAssetOrderTransactions " @@ -466,10 +466,11 @@ public class migrate extends common { case 11: // issue asset issueAssetPStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); issueAssetPStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("creator")))); - issueAssetPStmt.setString(3, (String) transaction.get("name")); - issueAssetPStmt.setString(4, (String) transaction.get("description")); - issueAssetPStmt.setBigDecimal(5, BigDecimal.valueOf(((Long) transaction.get("quantity")).longValue())); - issueAssetPStmt.setBoolean(6, (Boolean) transaction.get("divisible")); + issueAssetPStmt.setString(3, (String) transaction.get("owner")); + issueAssetPStmt.setString(4, (String) transaction.get("name")); + issueAssetPStmt.setString(5, (String) transaction.get("description")); + issueAssetPStmt.setBigDecimal(6, BigDecimal.valueOf(((Long) transaction.get("quantity")).longValue())); + issueAssetPStmt.setBoolean(7, (Boolean) transaction.get("divisible")); issueAssetPStmt.execute(); issueAssetPStmt.clearParameters(); diff --git a/src/utils/Serialization.java b/src/utils/Serialization.java index 0dda5bdb..34a81732 100644 --- a/src/utils/Serialization.java +++ b/src/utils/Serialization.java @@ -1,5 +1,6 @@ package utils; +import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; @@ -40,4 +41,19 @@ public class Serialization { return new PublicKeyAccount(bytes); } + public static String deserializeSizedString(ByteBuffer byteBuffer, int maxSize) throws ParseException { + int size = byteBuffer.getInt(); + if (size > maxSize || size > byteBuffer.remaining()) + throw new ParseException("Serialized string too long"); + + byte[] bytes = new byte[size]; + byteBuffer.get(bytes); + + try { + return new String(bytes, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new ParseException("UTF-8 charset unsupported during string deserialization"); + } + } + }