mirror of
https://github.com/Qortal/qortal.git
synced 2025-02-11 17:55:50 +00:00
IssueAssetTransactions
Use HSQLDB "CREATE TYPE" instead of "CREATE DOMAIN" as collate clause is lost on HSQLDB shutdown in v2.4.0. Restore GenesisAccount's public key back to 8-byte legacy value. More work on block/transaction processing. It's becoming apparent that way too many Connection objects are being passed around, and now with two forms of methods (one with, one without) it's time to switch to something like thread-local Connections. Maybe also switch to having data access objects. So this commit is save work prior to that conversion.
This commit is contained in:
parent
948bc95644
commit
3a6276c4a9
@ -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.
|
||||
* <p>
|
||||
* Typically for use within an ongoing SQL Transaction.
|
||||
* <p>
|
||||
* <b>Note: calls ResultSet.next()</b> 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.
|
||||
* <p>
|
||||
@ -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.
|
||||
* <p>
|
||||
* Typically for use within an ongoing SQL Transaction.
|
||||
* <p>
|
||||
* {@code whereClause} is SQL "WHERE" clause containing "?" placeholders suitable for use with PreparedStatements.
|
||||
* <p>
|
||||
* Example call:
|
||||
* <p>
|
||||
* {@code Connection connection = DB.getConnection();}<br>
|
||||
* {@code String manufacturer = "Lamborghini";}<br>
|
||||
* {@code int maxMileage = 100_000;}<br>
|
||||
* {@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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.
|
||||
* <p>
|
||||
* Performs various tests like checking for parent block, correct block timestamp, version, generating balance, etc.<br>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
351
src/qora/transaction/IssueAssetTransaction.java
Normal file
351
src/qora/transaction/IssueAssetTransaction.java
Normal file
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
90
src/test/exceptions.java
Normal file
90
src/test/exceptions.java
Normal file
@ -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.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user