diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs index 5f9305ff..287c6850 100644 --- a/.settings/org.eclipse.jdt.ui.prefs +++ b/.settings/org.eclipse.jdt.ui.prefs @@ -2,4 +2,4 @@ eclipse.preferences.version=1 formatter_profile=_Eclipse w/ indented switch formatter_settings_version=12 org.eclipse.jdt.ui.javadoc=false -org.eclipse.jdt.ui.text.custom_code_templates= +org.eclipse.jdt.ui.text.custom_code_templates= diff --git a/src/data/assets/AssetData.java b/src/data/assets/AssetData.java new file mode 100644 index 00000000..f4446fb2 --- /dev/null +++ b/src/data/assets/AssetData.java @@ -0,0 +1,64 @@ +package data.assets; + +public class AssetData { + + // Properties + private Long assetId; + private String owner; + private String name; + private String description; + private long quantity; + private boolean isDivisible; + private byte[] reference; + + // NOTE: key is Long, not long, because it can be null if asset ID/key not yet assigned. + public AssetData(Long assetId, String owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) { + this.assetId = assetId; + this.owner = owner; + this.name = name; + this.description = description; + this.quantity = quantity; + this.isDivisible = isDivisible; + this.reference = reference; + } + + // New asset with unassigned assetId + public AssetData(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 void setAssetId(Long assetId) { + this.assetId = assetId; + } + + public String getOwner() { + return this.owner; + } + + public String getName() { + return this.name; + } + + public String getDescription() { + return this.description; + } + + public long getQuantity() { + return this.quantity; + } + + public boolean getIsDivisible() { + return this.isDivisible; + } + + public byte[] getReference() { + return this.reference; + } + +} diff --git a/src/data/transaction/IssueAssetTransactionData.java b/src/data/transaction/IssueAssetTransactionData.java new file mode 100644 index 00000000..a08d5a9c --- /dev/null +++ b/src/data/transaction/IssueAssetTransactionData.java @@ -0,0 +1,77 @@ +package data.transaction; + +import java.math.BigDecimal; + +import qora.transaction.Transaction.TransactionType; + +public class IssueAssetTransactionData extends TransactionData { + + // Properties + // assetId can be null but assigned during save() or during load from repository + private Long assetId = null; + private byte[] issuerPublicKey; + private String owner; + private String assetName; + private String description; + private long quantity; + private boolean isDivisible; + + // Constructors + + public IssueAssetTransactionData(Long assetId, byte[] issuerPublicKey, String owner, String assetName, String description, long quantity, + boolean isDivisible, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + super(TransactionType.ISSUE_ASSET, fee, issuerPublicKey, timestamp, reference); + + this.assetId = assetId; + this.issuerPublicKey = issuerPublicKey; + this.owner = owner; + this.assetName = assetName; + this.description = description; + this.quantity = quantity; + this.isDivisible = isDivisible; + } + + public IssueAssetTransactionData(byte[] issuerPublicKey, String owner, String assetName, String description, long quantity, boolean isDivisible, + BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + this(null, issuerPublicKey, owner, assetName, description, quantity, isDivisible, fee, timestamp, reference, signature); + } + + // Getters/Setters + + public Long getAssetId() { + return this.assetId; + } + + public void setAssetId(Long assetId) { + this.assetId = assetId; + } + + public byte[] getIssuerPublicKey() { + return this.issuerPublicKey; + } + + public String getOwner() { + return this.owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getAssetName() { + return this.assetName; + } + + public String getDescription() { + return this.description; + } + + public long getQuantity() { + return this.quantity; + } + + public boolean getIsDivisible() { + return this.isDivisible; + } + +} diff --git a/src/qora/account/PublicKeyAccount.java b/src/qora/account/PublicKeyAccount.java index 2c90ac78..2762cd6f 100644 --- a/src/qora/account/PublicKeyAccount.java +++ b/src/qora/account/PublicKeyAccount.java @@ -23,8 +23,12 @@ public class PublicKeyAccount extends Account { } public boolean verify(byte[] signature, byte[] message) { + return PublicKeyAccount.verify(this.publicKey, signature, message); + } + + public static boolean verify(byte[] publicKey, byte[] signature, byte[] message) { try { - return Ed25519.verify(signature, message, this.publicKey); + return Ed25519.verify(signature, message, publicKey); } catch (Exception e) { return false; } diff --git a/src/qora/assets/Asset.java b/src/qora/assets/Asset.java index 450401b5..aaf25600 100644 --- a/src/qora/assets/Asset.java +++ b/src/qora/assets/Asset.java @@ -1,118 +1,10 @@ package qora.assets; -import java.sql.ResultSet; -import java.sql.SQLException; - -import database.DB; -import database.NoDataFoundException; -import qora.account.Account; -import repository.hsqldb.HSQLDBSaver; - public class Asset { + /** + * QORA coins are just another asset but with fixed assetId of zero. + */ public static final long QORA = 0L; - // Properties - private Long assetId; - private Account owner; - private String name; - private String description; - private long quantity; - private boolean isDivisible; - private byte[] reference; - - // 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, String owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) { - this.assetId = assetId; - this.owner = new Account(owner); - this.name = name; - this.description = description; - this.quantity = quantity; - this.isDivisible = isDivisible; - this.reference = reference; - } - - // New asset with unassigned assetId - 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 { - this(DB.checkedExecute("SELECT owner, asset_name, description, quantity, is_divisible, reference FROM Assets WHERE asset_id = ?", assetId)); - } - - protected Asset(ResultSet rs) throws SQLException { - if (rs == null) - throw new NoDataFoundException(); - - this.owner = new Account(rs.getString(1)); - this.name = rs.getString(2); - this.description = rs.getString(3); - this.quantity = rs.getLong(4); - this.isDivisible = rs.getBoolean(5); - this.reference = DB.getResultSetBytes(rs.getBinaryStream(6)); - } - - public static Asset fromAssetId(long assetId) throws SQLException { - try { - return new Asset(assetId); - } catch (NoDataFoundException e) { - return null; - } - } - - public void save() throws SQLException { - HSQLDBSaver saveHelper = new HSQLDBSaver("Assets"); - saveHelper.bind("asset_id", this.assetId).bind("owner", this.owner.getAddress()).bind("asset_name", this.name).bind("description", this.description) - .bind("quantity", this.quantity).bind("is_divisible", this.isDivisible).bind("reference", this.reference); - saveHelper.execute(); - - if (this.assetId == null) - this.assetId = DB.callIdentity(); - } - - public void delete() throws SQLException { - DB.checkedExecute("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(String assetName) throws SQLException { - return DB.exists("Assets", "asset_name = ?", assetName); - } - } diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index 76b6c77f..8a2caff5 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -206,7 +206,7 @@ public class Block { this.transactions = new ArrayList(); for (TransactionData transactionData : transactionsData) - this.transactions.add(Transaction.fromData(transactionData)); + this.transactions.add(Transaction.fromData(this.repository, transactionData)); return this.transactions; } @@ -242,7 +242,7 @@ public class Block { } // Add to block - this.transactions.add(Transaction.fromData(transactionData)); + this.transactions.add(Transaction.fromData(this.repository, transactionData)); // Update transaction count this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1); diff --git a/src/qora/block/BlockChain.java b/src/qora/block/BlockChain.java index 8577a2d4..e87c6682 100644 --- a/src/qora/block/BlockChain.java +++ b/src/qora/block/BlockChain.java @@ -3,6 +3,7 @@ package qora.block; import java.math.BigDecimal; import java.sql.SQLException; +import data.assets.AssetData; import data.block.BlockData; import qora.assets.Asset; import repository.BlockRepository; @@ -69,10 +70,9 @@ public class BlockChain { // Add QORA asset. // NOTE: Asset's transaction reference is Genesis Block's generator signature which doesn't exist as a transaction! - // TODO construct Asset(repository, AssetData) then .save()? - Asset qoraAsset = new Asset(Asset.QORA, genesisBlock.getGenerator().getAddress(), "Qora", "This is the simulated Qora asset.", 10_000_000_000L, true, + AssetData qoraAssetData = new AssetData(Asset.QORA, genesisBlock.getGenerator().getAddress(), "Qora", "This is the simulated Qora asset.", 10_000_000_000L, true, genesisBlock.getBlockData().getGeneratorSignature()); - qoraAsset.save(); + repository.getAssetRepository().save(qoraAssetData); repository.saveChanges(); } diff --git a/src/qora/block/GenesisBlock.java b/src/qora/block/GenesisBlock.java index 675681d6..ee8236c7 100644 --- a/src/qora/block/GenesisBlock.java +++ b/src/qora/block/GenesisBlock.java @@ -195,8 +195,8 @@ public class GenesisBlock extends Block { } private void addGenesisTransaction(String recipient, String amount) { - this.transactions - .add(Transaction.fromData(new GenesisTransactionData(recipient, new BigDecimal(amount).setScale(8), this.getBlockData().getTimestamp()))); + this.transactions.add(Transaction.fromData(this.repository, + new GenesisTransactionData(recipient, new BigDecimal(amount).setScale(8), this.getBlockData().getTimestamp()))); this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1); } diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java index c5bd05b0..b617f4f8 100644 --- a/src/qora/transaction/GenesisTransaction.java +++ b/src/qora/transaction/GenesisTransaction.java @@ -9,13 +9,16 @@ import data.transaction.GenesisTransactionData; import data.transaction.TransactionData; import qora.account.PrivateKeyAccount; import qora.crypto.Crypto; +import repository.Repository; import transform.TransformationException; import transform.transaction.TransactionTransformer; public class GenesisTransaction extends Transaction { - public GenesisTransaction(TransactionData transactionData) { - this.transactionData = transactionData; + // Constructors + + public GenesisTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); } // Processing @@ -68,14 +71,14 @@ public class GenesisTransaction extends Transaction { @Override public ValidationResult isValid() { - GenesisTransactionData genesisTransaction = (GenesisTransactionData) this.transactionData; + GenesisTransactionData genesisTransactionData = (GenesisTransactionData) this.transactionData; // Check amount is zero or positive - if (genesisTransaction.getAmount().compareTo(BigDecimal.ZERO) == -1) + if (genesisTransactionData.getAmount().compareTo(BigDecimal.ZERO) == -1) return ValidationResult.NEGATIVE_AMOUNT; // Check recipient address is valid - if (!Crypto.isValidAddress(genesisTransaction.getRecipient())) + if (!Crypto.isValidAddress(genesisTransactionData.getRecipient())) return ValidationResult.INVALID_ADDRESS; return ValidationResult.OK; diff --git a/src/qora/transaction/IssueAssetTransaction.java b/src/qora/transaction/IssueAssetTransaction.java index 3a5d4b12..2a5306be 100644 --- a/src/qora/transaction/IssueAssetTransaction.java +++ b/src/qora/transaction/IssueAssetTransaction.java @@ -1,350 +1,126 @@ package qora.transaction; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.math.BigDecimal; -import java.nio.ByteBuffer; -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 data.assets.AssetData; +import data.transaction.IssueAssetTransactionData; +import data.transaction.TransactionData; import qora.account.Account; import qora.account.PublicKeyAccount; import qora.assets.Asset; import qora.block.Block; import qora.crypto.Crypto; -import repository.hsqldb.HSQLDBSaver; -import transform.TransformationException; -import utils.Base58; +import repository.DataException; +import repository.Repository; +import transform.transaction.IssueAssetTransactionTransformer; import utils.NTP; -import utils.Serialization; -public class IssueAssetTransaction extends TransactionHandler { - - // 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; +public class IssueAssetTransaction extends Transaction { // 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() throws SQLException { - super.save(); - - HSQLDBSaver saveHelper = new HSQLDBSaver("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 TransactionHandler parse(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_LENGTH) - throw new TransformationException("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 TransformationException("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); - } + public IssueAssetTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); } // Processing - public ValidationResult isValid() throws SQLException { + public ValidationResult isValid() throws DataException { // Lowest cost checks first + IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) this.transactionData; + // 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())) + if (!Crypto.isValidAddress(issueAssetTransactionData.getOwner())) return ValidationResult.INVALID_ADDRESS; // Check name size bounds - if (this.assetName.length() < 1 || this.assetName.length() > MAX_NAME_SIZE) + if (issueAssetTransactionData.getAssetName().length() < 1 + || issueAssetTransactionData.getAssetName().length() > IssueAssetTransactionTransformer.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; // Check description size bounds - if (this.description.length() < 1 || this.description.length() > MAX_NAME_SIZE) + if (issueAssetTransactionData.getDescription().length() < 1 + || issueAssetTransactionData.getDescription().length() > IssueAssetTransactionTransformer.MAX_DESCRIPTION_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) + long maxQuantity = issueAssetTransactionData.getIsDivisible() ? 10_000_000_000L : 1_000_000_000_000_000_000L; + if (issueAssetTransactionData.getQuantity() < 1 || issueAssetTransactionData.getQuantity() > maxQuantity) return ValidationResult.INVALID_QUANTITY; // Check fee is positive - if (this.fee.compareTo(BigDecimal.ZERO) <= 0) + if (issueAssetTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) return ValidationResult.NEGATIVE_FEE; // Check reference is correct - if (!Arrays.equals(this.issuer.getLastReference(), this.reference)) + PublicKeyAccount issuer = new PublicKeyAccount(this.repository, issueAssetTransactionData.getIssuerPublicKey()); + + if (!Arrays.equals(issuer.getLastReference(), issueAssetTransactionData.getReference())) return ValidationResult.INVALID_REFERENCE; // Check issuer has enough funds - if (this.issuer.getConfirmedBalance(Asset.QORA).compareTo(this.fee) == -1) + if (issuer.getConfirmedBalance(Asset.QORA).compareTo(issueAssetTransactionData.getFee()) == -1) return ValidationResult.NO_BALANCE; - // XXX: Surely we want to check the asset name isn't already taken? - if (Asset.exists(this.assetName)) + // XXX: Surely we want to check the asset name isn't already taken? This check is not present in gen1. + if (this.repository.getAssetRepository().assetExists(issueAssetTransactionData.getAssetName())) return ValidationResult.ASSET_ALREADY_EXISTS; return ValidationResult.OK; } - public void process() throws SQLException { + public void process() throws DataException { + IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) this.transactionData; + // Issue asset - Asset asset = new Asset(owner.getAddress(), this.assetName, this.description, this.quantity, this.isDivisible, this.reference); - asset.save(); + AssetData assetData = new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(), + issueAssetTransactionData.getDescription(), issueAssetTransactionData.getQuantity(), issueAssetTransactionData.getIsDivisible(), + issueAssetTransactionData.getReference()); + this.repository.getAssetRepository().save(assetData); // Note newly assigned asset ID in our transaction record - this.assetId = asset.getAssetId(); + issueAssetTransactionData.setAssetId(assetData.getAssetId()); - this.save(); + // Save this transaction, now with corresponding assetId + this.repository.getTransactionRepository().save(issueAssetTransactionData); // Update issuer's balance - this.issuer.setConfirmedBalance(Asset.QORA, this.issuer.getConfirmedBalance(Asset.QORA).subtract(this.fee)); + Account issuer = new PublicKeyAccount(this.repository, issueAssetTransactionData.getIssuerPublicKey()); + issuer.setConfirmedBalance(Asset.QORA, issuer.getConfirmedBalance(Asset.QORA).subtract(issueAssetTransactionData.getFee())); // Update issuer's reference - this.issuer.setLastReference(this.signature); + issuer.setLastReference(issueAssetTransactionData.getSignature()); // Add asset to owner - this.owner.setConfirmedBalance(this.assetId, BigDecimal.valueOf(this.quantity).setScale(8)); + Account owner = new Account(this.repository, issueAssetTransactionData.getOwner()); + owner.setConfirmedBalance(issueAssetTransactionData.getAssetId(), BigDecimal.valueOf(issueAssetTransactionData.getQuantity()).setScale(8)); } - public void orphan() throws SQLException { + public void orphan() throws DataException { + IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) this.transactionData; + // Remove asset from owner - this.owner.deleteBalance(this.assetId); + Account owner = new Account(this.repository, issueAssetTransactionData.getOwner()); + owner.deleteBalance(issueAssetTransactionData.getAssetId()); // Unissue asset - Asset asset = Asset.fromAssetId(this.assetId); - asset.delete(); + this.repository.getAssetRepository().delete(issueAssetTransactionData.getAssetId()); - this.delete(); + // Delete this transaction itself + this.repository.getTransactionRepository().delete(issueAssetTransactionData); // Update issuer's balance - this.issuer.setConfirmedBalance(Asset.QORA, this.issuer.getConfirmedBalance(Asset.QORA).add(this.fee)); + Account issuer = new PublicKeyAccount(this.repository, issueAssetTransactionData.getIssuerPublicKey()); + issuer.setConfirmedBalance(Asset.QORA, issuer.getConfirmedBalance(Asset.QORA).add(issueAssetTransactionData.getFee())); // Update issuer's reference - this.issuer.setLastReference(this.reference); + issuer.setLastReference(issueAssetTransactionData.getReference()); } } diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index 769a0b7f..cfd75f4b 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -10,8 +10,10 @@ import static java.util.stream.Collectors.toMap; import data.block.BlockData; import data.transaction.TransactionData; import qora.account.PrivateKeyAccount; +import qora.account.PublicKeyAccount; import qora.block.Block; import qora.block.BlockChain; +import repository.DataException; import repository.Repository; import repository.RepositoryManager; import settings.Settings; @@ -65,14 +67,23 @@ public abstract class Transaction { protected static final BigDecimal minFeePerByte = BigDecimal.ONE.divide(maxBytePerFee, MathContext.DECIMAL32); // Properties + protected Repository repository; protected TransactionData transactionData; // Constructors - public static Transaction fromData(TransactionData transactionData) { + protected Transaction(Repository repository, TransactionData transactionData) { + this.repository = repository; + this.transactionData = transactionData; + } + + public static Transaction fromData(Repository repository, TransactionData transactionData) { switch (transactionData.getType()) { case GENESIS: - return new GenesisTransaction(transactionData); + return new GenesisTransaction(repository, transactionData); + + case ISSUE_ASSET: + return new IssueAssetTransaction(repository, transactionData); default: return null; @@ -142,20 +153,21 @@ public abstract class Transaction { * @return height, or 0 if not in blockchain (i.e. unconfirmed) */ public int getHeight() { - return RepositoryManager.getRepository().getTransactionRepository().getHeight(this.transactionData); + return this.repository.getTransactionRepository().getHeight(this.transactionData); } /** * Get number of confirmations for this transaction. * * @return confirmation count, or 0 if not in blockchain (i.e. unconfirmed) + * @throws DataException */ - public int getConfirmations() { + public int getConfirmations() throws DataException { int ourHeight = getHeight(); if (ourHeight == 0) return 0; - int blockChainHeight = BlockChain.getHeight(); + int blockChainHeight = this.repository.getBlockRepository().getBlockchainHeight(); if (blockChainHeight == 0) return 0; @@ -170,33 +182,35 @@ public abstract class Transaction { * @return Block, or null if transaction is not in a Block */ public BlockData getBlock() { - return RepositoryManager.getTransactionRepository().toBlock(this.transactionData); + return this.repository.getTransactionRepository().toBlock(this.transactionData); } /** * Load parent Transaction from DB via this transaction's reference. * * @return Transaction, or null if no parent found (which should not happen) + * @throws DataException */ - public TransactionData getParent() { + public TransactionData getParent() throws DataException { byte[] reference = this.transactionData.getReference(); if (reference == null) return null; - return RepositoryManager.getTransactionRepository().fromSignature(reference); + return this.repository.getTransactionRepository().fromSignature(reference); } /** * Load child Transaction from DB, if any. * * @return Transaction, or null if no child found + * @throws DataException */ - public TransactionData getChild() { + public TransactionData getChild() throws DataException { byte[] signature = this.transactionData.getSignature(); if (signature == null) return null; - return RepositoryManager.getTransactionRepository().fromSignature(signature); + return this.repository.getTransactionRepository().fromSignature(signature); } /** @@ -227,8 +241,7 @@ public abstract class Transaction { if (signature == null) return false; - // XXX: return this.transaction.getCreator().verify(signature, this.toBytesLessSignature()); - return false; + return PublicKeyAccount.verify(this.transactionData.getCreatorPublicKey(), signature, this.toBytesLessSignature()); } /** @@ -236,30 +249,28 @@ public abstract class Transaction { *

* Checks if transaction can have {@link TransactionHandler#process()} called. *

- * Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}. - *

* Transactions that have already been processed will return false. * * @return true if transaction can be processed, false otherwise */ - public abstract ValidationResult isValid(); + public abstract ValidationResult isValid() throws DataException; /** * Actually process a transaction, updating the blockchain. *

* Processes transaction, updating balances, references, assets, etc. as appropriate. - *

- * Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}. + * + * @throws DataException */ - public abstract void process(); + public abstract void process() throws DataException; /** * Undo transaction, updating the blockchain. *

* Undoes transaction, updating balances, references, assets, etc. as appropriate. - *

- * Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}. + * + * @throws DataException */ - public abstract void orphan(); + public abstract void orphan() throws DataException; } diff --git a/src/repository/AccountRepository.java b/src/repository/AccountRepository.java index 08f7b72b..d740fe80 100644 --- a/src/repository/AccountRepository.java +++ b/src/repository/AccountRepository.java @@ -6,15 +6,15 @@ import data.account.AccountData; public interface AccountRepository { // General account - + public AccountData getAccount(String address) throws DataException; - + public void save(AccountData accountData) throws DataException; // Account balances - + public AccountBalanceData getBalance(String address, long assetId) throws DataException; - + public void save(AccountBalanceData accountBalanceData) throws DataException; public void delete(String address, long assetId) throws DataException; diff --git a/src/repository/AssetRepository.java b/src/repository/AssetRepository.java new file mode 100644 index 00000000..7b2ff7e6 --- /dev/null +++ b/src/repository/AssetRepository.java @@ -0,0 +1,17 @@ +package repository; + +import data.assets.AssetData; + +public interface AssetRepository { + + public AssetData fromAssetId(long assetId) throws DataException; + + public boolean assetExists(long assetId) throws DataException; + + public boolean assetExists(String assetName) throws DataException; + + public void save(AssetData assetData) throws DataException; + + public void delete(long assetId) throws DataException; + +} diff --git a/src/repository/Repository.java b/src/repository/Repository.java index f607f6e0..9a33c9ca 100644 --- a/src/repository/Repository.java +++ b/src/repository/Repository.java @@ -4,6 +4,8 @@ public interface Repository { public AccountRepository getAccountRepository(); + public AssetRepository getAssetRepository(); + public BlockRepository getBlockRepository(); public TransactionRepository getTransactionRepository(); diff --git a/src/repository/TransactionRepository.java b/src/repository/TransactionRepository.java index b01b0ec6..334d107d 100644 --- a/src/repository/TransactionRepository.java +++ b/src/repository/TransactionRepository.java @@ -5,14 +5,14 @@ import data.block.BlockData; public interface TransactionRepository { - public TransactionData fromSignature(byte[] signature); + public TransactionData fromSignature(byte[] signature) throws DataException; - public TransactionData fromReference(byte[] reference); + public TransactionData fromReference(byte[] reference) throws DataException; public int getHeight(TransactionData transactionData); - + public BlockData toBlock(TransactionData transactionData); - + public void save(TransactionData transactionData) throws DataException; public void delete(TransactionData transactionData) throws DataException; diff --git a/src/repository/hsqldb/HSQLDBAccountRepository.java b/src/repository/hsqldb/HSQLDBAccountRepository.java index d27da759..80c543b5 100644 --- a/src/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/repository/hsqldb/HSQLDBAccountRepository.java @@ -34,7 +34,7 @@ public class HSQLDBAccountRepository implements AccountRepository { saveHelper.bind("account", accountData.getAddress()).bind("reference", accountData.getReference()); try { - saveHelper.execute(this.repository.connection); + saveHelper.execute(this.repository); } catch (SQLException e) { throw new DataException("Unable to save account info into repository", e); } @@ -60,7 +60,7 @@ public class HSQLDBAccountRepository implements AccountRepository { accountBalanceData.getBalance()); try { - saveHelper.execute(this.repository.connection); + saveHelper.execute(this.repository); } catch (SQLException e) { throw new DataException("Unable to save account balance into repository", e); } diff --git a/src/repository/hsqldb/HSQLDBAssetRepository.java b/src/repository/hsqldb/HSQLDBAssetRepository.java new file mode 100644 index 00000000..d2e69296 --- /dev/null +++ b/src/repository/hsqldb/HSQLDBAssetRepository.java @@ -0,0 +1,78 @@ +package repository.hsqldb; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import data.assets.AssetData; +import repository.AssetRepository; +import repository.DataException; + +public class HSQLDBAssetRepository implements AssetRepository { + + protected HSQLDBRepository repository; + + public HSQLDBAssetRepository(HSQLDBRepository repository) { + this.repository = repository; + } + + public AssetData fromAssetId(long assetId) throws DataException { + try { + ResultSet resultSet = this.repository + .checkedExecute("SELECT owner, asset_name, description, quantity, is_divisible, reference FROM Assets WHERE asset_id = ?", assetId); + if (resultSet == null) + return null; + + String owner = resultSet.getString(1); + String assetName = resultSet.getString(2); + String description = resultSet.getString(3); + long quantity = resultSet.getLong(4); + boolean isDivisible = resultSet.getBoolean(5); + byte[] reference = this.repository.getResultSetBytes(resultSet.getBinaryStream(6)); + + return new AssetData(assetId, owner, assetName, description, quantity, isDivisible, reference); + } catch (SQLException e) { + throw new DataException("Unable to fetch asset from repository", e); + } + } + + public boolean assetExists(long assetId) throws DataException { + try { + return this.repository.exists("Assets", "asset_id = ?", assetId); + } catch (SQLException e) { + throw new DataException("Unable to check for asset in repository", e); + } + } + + public boolean assetExists(String assetName) throws DataException { + try { + return this.repository.exists("Assets", "asset_name = ?", assetName); + } catch (SQLException e) { + throw new DataException("Unable to check for asset in repository", e); + } + } + + public void save(AssetData assetData) throws DataException { + HSQLDBSaver saveHelper = new HSQLDBSaver("Assets"); + saveHelper.bind("asset_id", assetData.getAssetId()).bind("owner", assetData.getOwner()).bind("asset_name", assetData.getName()) + .bind("description", assetData.getDescription()).bind("quantity", assetData.getQuantity()).bind("is_divisible", assetData.getIsDivisible()) + .bind("reference", assetData.getReference()); + + try { + saveHelper.execute(this.repository); + + if (assetData.getAssetId() == null) + assetData.setAssetId(this.repository.callIdentity()); + } catch (SQLException e) { + throw new DataException("Unable to save asset into repository", e); + } + } + + public void delete(long assetId) throws DataException { + try { + this.repository.checkedExecute("DELETE FROM Assets WHERE assetId = ?", assetId); + } catch (SQLException e) { + throw new DataException("Unable to delete asset from repository", e); + } + } + +} diff --git a/src/repository/hsqldb/HSQLDBBlockRepository.java b/src/repository/hsqldb/HSQLDBBlockRepository.java index f60c8408..8887ce8d 100644 --- a/src/repository/hsqldb/HSQLDBBlockRepository.java +++ b/src/repository/hsqldb/HSQLDBBlockRepository.java @@ -134,7 +134,7 @@ public class HSQLDBBlockRepository implements BlockRepository { .bind("AT_data", blockData.getAtBytes()).bind("AT_fees", blockData.getAtFees()); try { - saveHelper.execute(this.repository.connection); + saveHelper.execute(this.repository); } catch (SQLException e) { throw new DataException("Unable to save Block into repository", e); } @@ -146,7 +146,7 @@ public class HSQLDBBlockRepository implements BlockRepository { .bind("transaction_signature", blockTransactionData.getTransactionSignature()); try { - saveHelper.execute(this.repository.connection); + saveHelper.execute(this.repository); } catch (SQLException e) { throw new DataException("Unable to save BlockTransaction into repository", e); } diff --git a/src/repository/hsqldb/HSQLDBRepository.java b/src/repository/hsqldb/HSQLDBRepository.java index 7496d393..e2984203 100644 --- a/src/repository/hsqldb/HSQLDBRepository.java +++ b/src/repository/hsqldb/HSQLDBRepository.java @@ -9,10 +9,12 @@ import java.sql.ResultSet; import java.sql.SQLException; import repository.AccountRepository; +import repository.AssetRepository; import repository.BlockRepository; import repository.DataException; import repository.Repository; import repository.TransactionRepository; +import repository.hsqldb.transaction.HSQLDBTransactionRepository; public class HSQLDBRepository implements Repository { @@ -28,6 +30,11 @@ public class HSQLDBRepository implements Repository { return new HSQLDBAccountRepository(this); } + @Override + public AssetRepository getAssetRepository() { + return new HSQLDBAssetRepository(this); + } + @Override public BlockRepository getBlockRepository() { return new HSQLDBBlockRepository(this); @@ -79,7 +86,7 @@ public class HSQLDBRepository implements Repository { * @param inputStream * @return byte[] */ - byte[] getResultSetBytes(InputStream inputStream) { + public byte[] getResultSetBytes(InputStream inputStream) { // inputStream could be null if database's column's value is null if (inputStream == null) return null; @@ -107,7 +114,7 @@ public class HSQLDBRepository implements Repository { * @return ResultSet, or null if there are no found rows * @throws SQLException */ - ResultSet checkedExecute(String sql, Object... objects) throws SQLException { + public ResultSet checkedExecute(String sql, Object... objects) throws SQLException { PreparedStatement preparedStatement = this.connection.prepareStatement(sql); for (int i = 0; i < objects.length; ++i) @@ -130,7 +137,7 @@ public class HSQLDBRepository implements Repository { * @return ResultSet, or null if there are no found rows * @throws SQLException */ - ResultSet checkedExecute(PreparedStatement preparedStatement) throws SQLException { + public ResultSet checkedExecute(PreparedStatement preparedStatement) throws SQLException { if (!preparedStatement.execute()) throw new SQLException("Fetching from database produced no results"); @@ -154,7 +161,7 @@ public class HSQLDBRepository implements Repository { * @return Long * @throws SQLException */ - Long callIdentity() throws SQLException { + public Long callIdentity() throws SQLException { PreparedStatement preparedStatement = this.connection.prepareStatement("CALL IDENTITY()"); ResultSet resultSet = this.checkedExecute(preparedStatement); if (resultSet == null) @@ -180,7 +187,7 @@ public class HSQLDBRepository implements Repository { * @return true if matching row found in database, false otherwise * @throws SQLException */ - boolean exists(String tableName, String whereClause, Object... objects) throws SQLException { + public boolean exists(String tableName, String whereClause, Object... objects) throws SQLException { PreparedStatement preparedStatement = this.connection .prepareStatement("SELECT TRUE FROM " + tableName + " WHERE " + whereClause + " ORDER BY NULL LIMIT 1"); ResultSet resultSet = this.checkedExecute(preparedStatement); diff --git a/src/repository/hsqldb/HSQLDBSaver.java b/src/repository/hsqldb/HSQLDBSaver.java index 705ee81b..0d6172dd 100644 --- a/src/repository/hsqldb/HSQLDBSaver.java +++ b/src/repository/hsqldb/HSQLDBSaver.java @@ -1,7 +1,6 @@ package repository.hsqldb; import java.math.BigDecimal; -import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.ArrayList; @@ -15,7 +14,7 @@ import java.util.List; *

* {@code SaveHelper helper = new SaveHelper("TableName"); }
* {@code helper.bind("column_name", someColumnValue).bind("column2", columnValue2); }
- * {@code helper.execute(); }
+ * {@code helper.execute(repository); }
* */ public class HSQLDBSaver { @@ -49,14 +48,17 @@ public class HSQLDBSaver { /** * Build PreparedStatement using bound column-value pairs then execute it. + * + * @param repository + * TODO + * @param repository * - * @param connection * @return the result from {@link PreparedStatement#execute()} * @throws SQLException */ - public boolean execute(Connection connection) throws SQLException { + public boolean execute(HSQLDBRepository repository) throws SQLException { String sql = this.formatInsertWithPlaceholders(); - PreparedStatement preparedStatement = connection.prepareStatement(sql); + PreparedStatement preparedStatement = repository.connection.prepareStatement(sql); this.bindValues(preparedStatement); diff --git a/src/repository/hsqldb/HSQLDBGenesisTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBGenesisTransactionRepository.java similarity index 55% rename from src/repository/hsqldb/HSQLDBGenesisTransactionRepository.java rename to src/repository/hsqldb/transaction/HSQLDBGenesisTransactionRepository.java index 5a97639b..f640848a 100644 --- a/src/repository/hsqldb/HSQLDBGenesisTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBGenesisTransactionRepository.java @@ -1,4 +1,4 @@ -package repository.hsqldb; +package repository.hsqldb.transaction; import java.math.BigDecimal; import java.sql.ResultSet; @@ -7,6 +7,8 @@ import java.sql.SQLException; import data.transaction.GenesisTransactionData; import data.transaction.TransactionData; import repository.DataException; +import repository.hsqldb.HSQLDBRepository; +import repository.hsqldb.HSQLDBSaver; public class HSQLDBGenesisTransactionRepository extends HSQLDBTransactionRepository { @@ -14,7 +16,7 @@ public class HSQLDBGenesisTransactionRepository extends HSQLDBTransactionReposit super(repository); } - TransactionData fromBase(byte[] signature, byte[] reference, byte[] creator, long timestamp, BigDecimal fee) { + TransactionData fromBase(byte[] signature, byte[] reference, byte[] creator, long timestamp, BigDecimal fee) throws DataException { try { ResultSet rs = this.repository.checkedExecute("SELECT recipient, amount FROM GenesisTransactions WHERE signature = ?", signature); if (rs == null) @@ -25,22 +27,24 @@ public class HSQLDBGenesisTransactionRepository extends HSQLDBTransactionReposit return new GenesisTransactionData(recipient, amount, timestamp, signature); } catch (SQLException e) { - return null; + throw new DataException("Unable to fetch genesis transaction from repository", e); } } @Override - public void save(TransactionData transaction) throws DataException { - super.save(transaction); + public void save(TransactionData transactionData) throws DataException { + super.save(transactionData); + + GenesisTransactionData genesisTransactionData = (GenesisTransactionData) transactionData; - GenesisTransactionData genesisTransaction = (GenesisTransactionData) transaction; - HSQLDBSaver saveHelper = new HSQLDBSaver("GenesisTransactions"); - saveHelper.bind("signature", genesisTransaction.getSignature()).bind("recipient", genesisTransaction.getRecipient()).bind("amount", genesisTransaction.getAmount()); + saveHelper.bind("signature", genesisTransactionData.getSignature()).bind("recipient", genesisTransactionData.getRecipient()).bind("amount", + genesisTransactionData.getAmount()); + try { - saveHelper.execute(this.repository.connection); + saveHelper.execute(this.repository); } catch (SQLException e) { - throw new DataException(e); + throw new DataException("Unable to save genesis transaction into repository", e); } } diff --git a/src/repository/hsqldb/transaction/HSQLDBIssueAssetTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBIssueAssetTransactionRepository.java new file mode 100644 index 00000000..4a32ca26 --- /dev/null +++ b/src/repository/hsqldb/transaction/HSQLDBIssueAssetTransactionRepository.java @@ -0,0 +1,62 @@ +package repository.hsqldb.transaction; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import data.transaction.IssueAssetTransactionData; +import data.transaction.TransactionData; +import repository.DataException; +import repository.hsqldb.HSQLDBRepository; +import repository.hsqldb.HSQLDBSaver; + +public class HSQLDBIssueAssetTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBIssueAssetTransactionRepository(HSQLDBRepository repository) { + super(repository); + } + + TransactionData fromBase(byte[] signature, byte[] reference, byte[] creator, long timestamp, BigDecimal fee) throws DataException { + try { + ResultSet rs = this.repository.checkedExecute( + "SELECT issuer, owner, asset_name, description, quantity, is_divisible, asset_id FROM IssueAssetTransactions WHERE signature = ?", + signature); + if (rs == null) + return null; + + byte[] issuerPublicKey = this.repository.getResultSetBytes(rs.getBinaryStream(1)); + String owner = rs.getString(2); + String assetName = rs.getString(3); + String description = rs.getString(4); + long quantity = rs.getLong(5); + boolean isDivisible = rs.getBoolean(6); + Long assetId = rs.getLong(7); + + return new IssueAssetTransactionData(assetId, issuerPublicKey, owner, assetName, description, quantity, isDivisible, fee, timestamp, reference, + signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch issue asset transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + super.save(transactionData); + + IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("IssueAssetTransactions"); + + saveHelper.bind("signature", issueAssetTransactionData.getSignature()).bind("issuer", issueAssetTransactionData.getIssuerPublicKey()) + .bind("asset_name", issueAssetTransactionData.getAssetName()).bind("description", issueAssetTransactionData.getDescription()) + .bind("quantity", issueAssetTransactionData.getQuantity()).bind("is_divisible", issueAssetTransactionData.getIsDivisible()) + .bind("asset_id", issueAssetTransactionData.getAssetId()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save issue asset transaction into repository", e); + } + } + +} diff --git a/src/repository/hsqldb/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java similarity index 84% rename from src/repository/hsqldb/HSQLDBTransactionRepository.java rename to src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index c4e60ce2..ad4894d7 100644 --- a/src/repository/hsqldb/HSQLDBTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -1,4 +1,4 @@ -package repository.hsqldb; +package repository.hsqldb.transaction; import java.math.BigDecimal; import java.sql.ResultSet; @@ -10,18 +10,22 @@ import data.transaction.TransactionData; import qora.transaction.Transaction.TransactionType; import repository.DataException; import repository.TransactionRepository; +import repository.hsqldb.HSQLDBRepository; +import repository.hsqldb.HSQLDBSaver; public class HSQLDBTransactionRepository implements TransactionRepository { protected HSQLDBRepository repository; private HSQLDBGenesisTransactionRepository genesisTransactionRepository; + private HSQLDBIssueAssetTransactionRepository issueAssetTransactionRepository; public HSQLDBTransactionRepository(HSQLDBRepository repository) { this.repository = repository; genesisTransactionRepository = new HSQLDBGenesisTransactionRepository(repository); + issueAssetTransactionRepository = new HSQLDBIssueAssetTransactionRepository(repository); } - public TransactionData fromSignature(byte[] signature) { + public TransactionData fromSignature(byte[] signature) throws DataException { try { ResultSet rs = this.repository.checkedExecute("SELECT type, reference, creator, creation, fee FROM Transactions WHERE signature = ?", signature); if (rs == null) @@ -35,11 +39,11 @@ public class HSQLDBTransactionRepository implements TransactionRepository { return this.fromBase(type, signature, reference, creator, timestamp, fee); } catch (SQLException e) { - return null; + throw new DataException("Unable to fetch transaction from repository", e); } } - public TransactionData fromReference(byte[] reference) { + public TransactionData fromReference(byte[] reference) throws DataException { try { ResultSet rs = this.repository.checkedExecute("SELECT type, signature, creator, creation, fee FROM Transactions WHERE reference = ?", reference); if (rs == null) @@ -53,15 +57,19 @@ public class HSQLDBTransactionRepository implements TransactionRepository { return this.fromBase(type, signature, reference, creator, timestamp, fee); } catch (SQLException e) { - return null; + throw new DataException("Unable to fetch transaction from repository", e); } } - private TransactionData fromBase(TransactionType type, byte[] signature, byte[] reference, byte[] creator, long timestamp, BigDecimal fee) { + private TransactionData fromBase(TransactionType type, byte[] signature, byte[] reference, byte[] creator, long timestamp, BigDecimal fee) + throws DataException { switch (type) { case GENESIS: return this.genesisTransactionRepository.fromBase(signature, reference, creator, timestamp, fee); + case ISSUE_ASSET: + return this.issueAssetTransactionRepository.fromBase(signature, reference, creator, timestamp, fee); + default: return null; } @@ -115,7 +123,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { .bind("creator", transactionData.getCreatorPublicKey()).bind("creation", new Timestamp(transactionData.getTimestamp())) .bind("fee", transactionData.getFee()).bind("milestone_block", null); try { - saver.execute(this.repository.connection); + saver.execute(this.repository); } catch (SQLException e) { throw new DataException(e); } diff --git a/src/transform/Transformer.java b/src/transform/Transformer.java index d35e8b7f..c7b2b550 100644 --- a/src/transform/Transformer.java +++ b/src/transform/Transformer.java @@ -2,6 +2,7 @@ package transform; public abstract class Transformer { + public static final int BOOLEAN_LENGTH = 4; public static final int INT_LENGTH = 4; public static final int LONG_LENGTH = 8; @@ -9,7 +10,7 @@ public abstract class Transformer { public static final int ADDRESS_LENGTH = 25; public static final int PUBLIC_KEY_LENGTH = 32; - public static final int SIGNATURE_LENGTH = 64; + public static final int SIGNATURE_LENGTH = 64; public static final int TIMESTAMP_LENGTH = LONG_LENGTH; } diff --git a/src/transform/transaction/GenesisTransactionTransformer.java b/src/transform/transaction/GenesisTransactionTransformer.java index 0baf4ece..d53b589e 100644 --- a/src/transform/transaction/GenesisTransactionTransformer.java +++ b/src/transform/transaction/GenesisTransactionTransformer.java @@ -35,20 +35,20 @@ public class GenesisTransactionTransformer extends TransactionTransformer { return new GenesisTransactionData(recipient, amount, timestamp); } - public static int getDataLength(TransactionData baseTransaction) throws TransformationException { + public static int getDataLength(TransactionData transactionData) throws TransformationException { return TYPE_LENGTH + TYPELESS_LENGTH; } - public static byte[] toBytes(TransactionData baseTransaction) throws TransformationException { + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { try { - GenesisTransactionData transaction = (GenesisTransactionData) baseTransaction; + GenesisTransactionData genesisTransactionData = (GenesisTransactionData) transactionData; ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - bytes.write(Ints.toByteArray(transaction.getType().value)); - bytes.write(Longs.toByteArray(transaction.getTimestamp())); - bytes.write(Base58.decode(transaction.getRecipient())); - bytes.write(Serialization.serializeBigDecimal(transaction.getAmount())); + bytes.write(Ints.toByteArray(genesisTransactionData.getType().value)); + bytes.write(Longs.toByteArray(genesisTransactionData.getTimestamp())); + bytes.write(Base58.decode(genesisTransactionData.getRecipient())); + bytes.write(Serialization.serializeBigDecimal(genesisTransactionData.getAmount())); return bytes.toByteArray(); } catch (IOException | ClassCastException e) { @@ -57,14 +57,14 @@ public class GenesisTransactionTransformer extends TransactionTransformer { } @SuppressWarnings("unchecked") - public static JSONObject toJSON(TransactionData baseTransaction) throws TransformationException { - JSONObject json = TransactionTransformer.getBaseJSON(baseTransaction); + public static JSONObject toJSON(TransactionData transactionData) throws TransformationException { + JSONObject json = TransactionTransformer.getBaseJSON(transactionData); try { - GenesisTransactionData transaction = (GenesisTransactionData) baseTransaction; + GenesisTransactionData genesisTransactionData = (GenesisTransactionData) transactionData; - json.put("recipient", transaction.getRecipient()); - json.put("amount", transaction.getAmount().toPlainString()); + json.put("recipient", genesisTransactionData.getRecipient()); + json.put("amount", genesisTransactionData.getAmount().toPlainString()); } catch (ClassCastException e) { throw new TransformationException(e); } diff --git a/src/transform/transaction/IssueAssetTransactionTransformer.java b/src/transform/transaction/IssueAssetTransactionTransformer.java new file mode 100644 index 00000000..bf206e9b --- /dev/null +++ b/src/transform/transaction/IssueAssetTransactionTransformer.java @@ -0,0 +1,122 @@ +package transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; + +import org.json.simple.JSONObject; + +import com.google.common.hash.HashCode; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +import data.transaction.TransactionData; +import qora.account.PublicKeyAccount; +import data.transaction.IssueAssetTransactionData; +import transform.TransformationException; +import utils.Base58; +import utils.Serialization; + +public class IssueAssetTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int ISSUER_LENGTH = PUBLIC_KEY_LENGTH; + private static final int OWNER_LENGTH = ADDRESS_LENGTH; + private static final int NAME_SIZE_LENGTH = INT_LENGTH; + private static final int DESCRIPTION_SIZE_LENGTH = INT_LENGTH; + private static final int QUANTITY_LENGTH = LONG_LENGTH; + private static final int IS_DIVISIBLE_LENGTH = BOOLEAN_LENGTH; + 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 + public static final int MAX_NAME_SIZE = 400; + public static final int MAX_DESCRIPTION_SIZE = 4000; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + if (byteBuffer.remaining() < TYPELESS_LENGTH) + throw new TransformationException("Byte data too short for GenesisTransaction"); + + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] 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 TransformationException("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 IssueAssetTransactionData(issuer, owner, assetName, description, quantity, isDivisible, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) transactionData; + + return TYPE_LENGTH + TYPELESS_LENGTH + issueAssetTransactionData.getAssetName().length() + issueAssetTransactionData.getDescription().length(); + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(issueAssetTransactionData.getType().value)); + bytes.write(Longs.toByteArray(issueAssetTransactionData.getTimestamp())); + bytes.write(issueAssetTransactionData.getReference()); + bytes.write(issueAssetTransactionData.getIssuerPublicKey()); + bytes.write(Base58.decode(issueAssetTransactionData.getOwner())); + + Serialization.serializeSizedString(bytes, issueAssetTransactionData.getAssetName()); + Serialization.serializeSizedString(bytes, issueAssetTransactionData.getDescription()); + + bytes.write(Longs.toByteArray(issueAssetTransactionData.getQuantity())); + bytes.write((byte) (issueAssetTransactionData.getIsDivisible() ? 1 : 0)); + + bytes.write(issueAssetTransactionData.getSignature()); + + return bytes.toByteArray(); + } catch (IOException | ClassCastException e) { + throw new TransformationException(e); + } + } + + @SuppressWarnings("unchecked") + public static JSONObject toJSON(TransactionData transactionData) throws TransformationException { + JSONObject json = TransactionTransformer.getBaseJSON(transactionData); + + try { + IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) transactionData; + + byte[] issuerPublicKey = issueAssetTransactionData.getIssuerPublicKey(); + + json.put("issuer", PublicKeyAccount.getAddress(issuerPublicKey)); + json.put("issuerPublicKey", HashCode.fromBytes(issuerPublicKey).toString()); + json.put("owner", issueAssetTransactionData.getOwner()); + json.put("assetName", issueAssetTransactionData.getAssetName()); + json.put("description", issueAssetTransactionData.getDescription()); + json.put("quantity", issueAssetTransactionData.getQuantity()); + json.put("isDivisible", issueAssetTransactionData.getIsDivisible()); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/transform/transaction/TransactionTransformer.java b/src/transform/transaction/TransactionTransformer.java index e118ab66..18dbf89b 100644 --- a/src/transform/transaction/TransactionTransformer.java +++ b/src/transform/transaction/TransactionTransformer.java @@ -13,6 +13,8 @@ import utils.Base58; public class TransactionTransformer extends Transformer { protected static final int TYPE_LENGTH = INT_LENGTH; + protected static final int REFERENCE_LENGTH = SIGNATURE_LENGTH; + protected static final int BASE_TYPELESS_LENGTH = TYPE_LENGTH + TIMESTAMP_LENGTH + REFERENCE_LENGTH + SIGNATURE_LENGTH; public static TransactionData fromBytes(byte[] bytes) throws TransformationException { if (bytes == null) @@ -31,25 +33,34 @@ public class TransactionTransformer extends Transformer { case GENESIS: return GenesisTransactionTransformer.fromByteBuffer(byteBuffer); + case ISSUE_ASSET: + return IssueAssetTransactionTransformer.fromByteBuffer(byteBuffer); + default: return null; } } - public static int getDataLength(TransactionData transaction) throws TransformationException { - switch (transaction.getType()) { + public static int getDataLength(TransactionData transactionData) throws TransformationException { + switch (transactionData.getType()) { case GENESIS: - return GenesisTransactionTransformer.getDataLength(transaction); + return GenesisTransactionTransformer.getDataLength(transactionData); + + case ISSUE_ASSET: + return IssueAssetTransactionTransformer.getDataLength(transactionData); default: throw new TransformationException("Unsupported transaction type"); } } - public static byte[] toBytes(TransactionData transaction) throws TransformationException { - switch (transaction.getType()) { + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + switch (transactionData.getType()) { case GENESIS: - return GenesisTransactionTransformer.toBytes(transaction); + return GenesisTransactionTransformer.toBytes(transactionData); + + case ISSUE_ASSET: + return IssueAssetTransactionTransformer.toBytes(transactionData); default: return null; diff --git a/src/utils/Serialization.java b/src/utils/Serialization.java index bb308926..980aff01 100644 --- a/src/utils/Serialization.java +++ b/src/utils/Serialization.java @@ -1,10 +1,14 @@ package utils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; +import com.google.common.primitives.Ints; + import transform.TransformationException; import transform.Transformer; @@ -41,6 +45,11 @@ public class Serialization { return bytes; } + public static void serializeSizedString(ByteArrayOutputStream bytes, String string) throws UnsupportedEncodingException, IOException { + bytes.write(Ints.toByteArray(string.length())); + bytes.write(string.getBytes("UTF-8")); + } + public static String deserializeSizedString(ByteBuffer byteBuffer, int maxSize) throws TransformationException { int size = byteBuffer.getInt(); if (size > maxSize || size > byteBuffer.remaining())