3
0
mirror of https://github.com/Qortal/qortal.git synced 2025-02-11 17:55:50 +00:00

Work on Assets conversion

* Added AssetData transfer object
* Added IssueAssetTransactionData transfer object

* Reworked qora.assets.Asset into business layer object
* Reworked qora.transaction.IssueAssetTransaction into business layer object

* Added corresponding AssetRepository and support in TransactionRepository et al

* Fixed BlockChain in line with asset changes

* Some renaming inside GenesisTransaction to reflect use of transfer object, not business object

* Business transaction objects now take Repository param

* Moved HSQLDB transaction repositories into a sub-package
* Changed HSQLDBSaver.execute(Connection connection) to .execute(Repository repository) to fix visibility issues
and allow repository more control in the future if need be

* Changed from "return null" statements in HSQLDB repositories to throw DataException when an error occurs.
Better to throw than to silently return null?

* Added static version of PublicKeyAccount.verify() for when a repository-backed PublicKeyAccount is not needed

* Fixed getter/setter code template incorrectly producing "this.this.field = param"
This commit is contained in:
catbref 2018-06-13 11:46:33 +01:00
parent 698c4b6cc9
commit 519331f823
28 changed files with 630 additions and 480 deletions

File diff suppressed because one or more lines are too long

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -23,8 +23,12 @@ public class PublicKeyAccount extends Account {
} }
public boolean verify(byte[] signature, byte[] message) { 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 { try {
return Ed25519.verify(signature, message, this.publicKey); return Ed25519.verify(signature, message, publicKey);
} catch (Exception e) { } catch (Exception e) {
return false; return false;
} }

View File

@ -1,118 +1,10 @@
package qora.assets; 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 { public class Asset {
/**
* QORA coins are just another asset but with fixed assetId of zero.
*/
public static final long QORA = 0L; 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);
}
} }

View File

@ -206,7 +206,7 @@ public class Block {
this.transactions = new ArrayList<Transaction>(); this.transactions = new ArrayList<Transaction>();
for (TransactionData transactionData : transactionsData) for (TransactionData transactionData : transactionsData)
this.transactions.add(Transaction.fromData(transactionData)); this.transactions.add(Transaction.fromData(this.repository, transactionData));
return this.transactions; return this.transactions;
} }
@ -242,7 +242,7 @@ public class Block {
} }
// Add to block // Add to block
this.transactions.add(Transaction.fromData(transactionData)); this.transactions.add(Transaction.fromData(this.repository, transactionData));
// Update transaction count // Update transaction count
this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1); this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1);

View File

@ -3,6 +3,7 @@ package qora.block;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.sql.SQLException; import java.sql.SQLException;
import data.assets.AssetData;
import data.block.BlockData; import data.block.BlockData;
import qora.assets.Asset; import qora.assets.Asset;
import repository.BlockRepository; import repository.BlockRepository;
@ -69,10 +70,9 @@ public class BlockChain {
// Add QORA asset. // Add QORA asset.
// NOTE: Asset's transaction reference is Genesis Block's generator signature which doesn't exist as a transaction! // 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()? AssetData qoraAssetData = new AssetData(Asset.QORA, genesisBlock.getGenerator().getAddress(), "Qora", "This is the simulated Qora asset.", 10_000_000_000L, true,
Asset qoraAsset = new Asset(Asset.QORA, genesisBlock.getGenerator().getAddress(), "Qora", "This is the simulated Qora asset.", 10_000_000_000L, true,
genesisBlock.getBlockData().getGeneratorSignature()); genesisBlock.getBlockData().getGeneratorSignature());
qoraAsset.save(); repository.getAssetRepository().save(qoraAssetData);
repository.saveChanges(); repository.saveChanges();
} }

View File

@ -195,8 +195,8 @@ public class GenesisBlock extends Block {
} }
private void addGenesisTransaction(String recipient, String amount) { private void addGenesisTransaction(String recipient, String amount) {
this.transactions this.transactions.add(Transaction.fromData(this.repository,
.add(Transaction.fromData(new GenesisTransactionData(recipient, new BigDecimal(amount).setScale(8), this.getBlockData().getTimestamp()))); new GenesisTransactionData(recipient, new BigDecimal(amount).setScale(8), this.getBlockData().getTimestamp())));
this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1); this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1);
} }

View File

@ -9,13 +9,16 @@ import data.transaction.GenesisTransactionData;
import data.transaction.TransactionData; import data.transaction.TransactionData;
import qora.account.PrivateKeyAccount; import qora.account.PrivateKeyAccount;
import qora.crypto.Crypto; import qora.crypto.Crypto;
import repository.Repository;
import transform.TransformationException; import transform.TransformationException;
import transform.transaction.TransactionTransformer; import transform.transaction.TransactionTransformer;
public class GenesisTransaction extends Transaction { public class GenesisTransaction extends Transaction {
public GenesisTransaction(TransactionData transactionData) { // Constructors
this.transactionData = transactionData;
public GenesisTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData);
} }
// Processing // Processing
@ -68,14 +71,14 @@ public class GenesisTransaction extends Transaction {
@Override @Override
public ValidationResult isValid() { public ValidationResult isValid() {
GenesisTransactionData genesisTransaction = (GenesisTransactionData) this.transactionData; GenesisTransactionData genesisTransactionData = (GenesisTransactionData) this.transactionData;
// Check amount is zero or positive // Check amount is zero or positive
if (genesisTransaction.getAmount().compareTo(BigDecimal.ZERO) == -1) if (genesisTransactionData.getAmount().compareTo(BigDecimal.ZERO) == -1)
return ValidationResult.NEGATIVE_AMOUNT; return ValidationResult.NEGATIVE_AMOUNT;
// Check recipient address is valid // Check recipient address is valid
if (!Crypto.isValidAddress(genesisTransaction.getRecipient())) if (!Crypto.isValidAddress(genesisTransactionData.getRecipient()))
return ValidationResult.INVALID_ADDRESS; return ValidationResult.INVALID_ADDRESS;
return ValidationResult.OK; return ValidationResult.OK;

View File

@ -1,350 +1,126 @@
package qora.transaction; package qora.transaction;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays; import java.util.Arrays;
import org.json.simple.JSONObject; import data.assets.AssetData;
import data.transaction.IssueAssetTransactionData;
import com.google.common.hash.HashCode; import data.transaction.TransactionData;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import database.DB;
import database.NoDataFoundException;
import qora.account.Account; import qora.account.Account;
import qora.account.PublicKeyAccount; import qora.account.PublicKeyAccount;
import qora.assets.Asset; import qora.assets.Asset;
import qora.block.Block; import qora.block.Block;
import qora.crypto.Crypto; import qora.crypto.Crypto;
import repository.hsqldb.HSQLDBSaver; import repository.DataException;
import transform.TransformationException; import repository.Repository;
import utils.Base58; import transform.transaction.IssueAssetTransactionTransformer;
import utils.NTP; import utils.NTP;
import utils.Serialization;
public class IssueAssetTransaction extends TransactionHandler { 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 // Constructors
/** public IssueAssetTransaction(Repository repository, TransactionData transactionData) {
* Reconstruct an IssueAssetTransaction, including signature. super(repository, transactionData);
*
* @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);
}
} }
// Processing // Processing
public ValidationResult isValid() throws SQLException { public ValidationResult isValid() throws DataException {
// Lowest cost checks first // Lowest cost checks first
IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) this.transactionData;
// Are IssueAssetTransactions even allowed at this point? // Are IssueAssetTransactions even allowed at this point?
if (NTP.getTime() < Block.ASSETS_RELEASE_TIMESTAMP) if (NTP.getTime() < Block.ASSETS_RELEASE_TIMESTAMP)
return ValidationResult.NOT_YET_RELEASED; return ValidationResult.NOT_YET_RELEASED;
// Check owner address is valid // Check owner address is valid
if (!Crypto.isValidAddress(this.owner.getAddress())) if (!Crypto.isValidAddress(issueAssetTransactionData.getOwner()))
return ValidationResult.INVALID_ADDRESS; return ValidationResult.INVALID_ADDRESS;
// Check name size bounds // 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; return ValidationResult.INVALID_NAME_LENGTH;
// Check description size bounds // 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; return ValidationResult.INVALID_DESCRIPTION_LENGTH;
// Check quantity - either 10 billion or if that's not enough: a billion billion! // 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; long maxQuantity = issueAssetTransactionData.getIsDivisible() ? 10_000_000_000L : 1_000_000_000_000_000_000L;
if (this.quantity < 1 || this.quantity > maxQuantity) if (issueAssetTransactionData.getQuantity() < 1 || issueAssetTransactionData.getQuantity() > maxQuantity)
return ValidationResult.INVALID_QUANTITY; return ValidationResult.INVALID_QUANTITY;
// Check fee is positive // Check fee is positive
if (this.fee.compareTo(BigDecimal.ZERO) <= 0) if (issueAssetTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_FEE; return ValidationResult.NEGATIVE_FEE;
// Check reference is correct // 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; return ValidationResult.INVALID_REFERENCE;
// Check issuer has enough funds // 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; return ValidationResult.NO_BALANCE;
// XXX: Surely we want to check the asset name isn't already taken? // XXX: Surely we want to check the asset name isn't already taken? This check is not present in gen1.
if (Asset.exists(this.assetName)) if (this.repository.getAssetRepository().assetExists(issueAssetTransactionData.getAssetName()))
return ValidationResult.ASSET_ALREADY_EXISTS; return ValidationResult.ASSET_ALREADY_EXISTS;
return ValidationResult.OK; return ValidationResult.OK;
} }
public void process() throws SQLException { public void process() throws DataException {
IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) this.transactionData;
// Issue asset // Issue asset
Asset asset = new Asset(owner.getAddress(), this.assetName, this.description, this.quantity, this.isDivisible, this.reference); AssetData assetData = new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(),
asset.save(); issueAssetTransactionData.getDescription(), issueAssetTransactionData.getQuantity(), issueAssetTransactionData.getIsDivisible(),
issueAssetTransactionData.getReference());
this.repository.getAssetRepository().save(assetData);
// Note newly assigned asset ID in our transaction record // 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 // 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 // Update issuer's reference
this.issuer.setLastReference(this.signature); issuer.setLastReference(issueAssetTransactionData.getSignature());
// Add asset to owner // 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 // Remove asset from owner
this.owner.deleteBalance(this.assetId); Account owner = new Account(this.repository, issueAssetTransactionData.getOwner());
owner.deleteBalance(issueAssetTransactionData.getAssetId());
// Unissue asset // Unissue asset
Asset asset = Asset.fromAssetId(this.assetId); this.repository.getAssetRepository().delete(issueAssetTransactionData.getAssetId());
asset.delete();
this.delete(); // Delete this transaction itself
this.repository.getTransactionRepository().delete(issueAssetTransactionData);
// Update issuer's balance // 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 // Update issuer's reference
this.issuer.setLastReference(this.reference); issuer.setLastReference(issueAssetTransactionData.getReference());
} }
} }

View File

@ -10,8 +10,10 @@ import static java.util.stream.Collectors.toMap;
import data.block.BlockData; import data.block.BlockData;
import data.transaction.TransactionData; import data.transaction.TransactionData;
import qora.account.PrivateKeyAccount; import qora.account.PrivateKeyAccount;
import qora.account.PublicKeyAccount;
import qora.block.Block; import qora.block.Block;
import qora.block.BlockChain; import qora.block.BlockChain;
import repository.DataException;
import repository.Repository; import repository.Repository;
import repository.RepositoryManager; import repository.RepositoryManager;
import settings.Settings; import settings.Settings;
@ -65,14 +67,23 @@ public abstract class Transaction {
protected static final BigDecimal minFeePerByte = BigDecimal.ONE.divide(maxBytePerFee, MathContext.DECIMAL32); protected static final BigDecimal minFeePerByte = BigDecimal.ONE.divide(maxBytePerFee, MathContext.DECIMAL32);
// Properties // Properties
protected Repository repository;
protected TransactionData transactionData; protected TransactionData transactionData;
// Constructors // 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()) { switch (transactionData.getType()) {
case GENESIS: case GENESIS:
return new GenesisTransaction(transactionData); return new GenesisTransaction(repository, transactionData);
case ISSUE_ASSET:
return new IssueAssetTransaction(repository, transactionData);
default: default:
return null; return null;
@ -142,20 +153,21 @@ public abstract class Transaction {
* @return height, or 0 if not in blockchain (i.e. unconfirmed) * @return height, or 0 if not in blockchain (i.e. unconfirmed)
*/ */
public int getHeight() { public int getHeight() {
return RepositoryManager.getRepository().getTransactionRepository().getHeight(this.transactionData); return this.repository.getTransactionRepository().getHeight(this.transactionData);
} }
/** /**
* Get number of confirmations for this transaction. * Get number of confirmations for this transaction.
* *
* @return confirmation count, or 0 if not in blockchain (i.e. unconfirmed) * @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(); int ourHeight = getHeight();
if (ourHeight == 0) if (ourHeight == 0)
return 0; return 0;
int blockChainHeight = BlockChain.getHeight(); int blockChainHeight = this.repository.getBlockRepository().getBlockchainHeight();
if (blockChainHeight == 0) if (blockChainHeight == 0)
return 0; return 0;
@ -170,33 +182,35 @@ public abstract class Transaction {
* @return Block, or null if transaction is not in a Block * @return Block, or null if transaction is not in a Block
*/ */
public BlockData getBlock() { 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. * Load parent Transaction from DB via this transaction's reference.
* *
* @return Transaction, or null if no parent found (which should not happen) * @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(); byte[] reference = this.transactionData.getReference();
if (reference == null) if (reference == null)
return null; return null;
return RepositoryManager.getTransactionRepository().fromSignature(reference); return this.repository.getTransactionRepository().fromSignature(reference);
} }
/** /**
* Load child Transaction from DB, if any. * Load child Transaction from DB, if any.
* *
* @return Transaction, or null if no child found * @return Transaction, or null if no child found
* @throws DataException
*/ */
public TransactionData getChild() { public TransactionData getChild() throws DataException {
byte[] signature = this.transactionData.getSignature(); byte[] signature = this.transactionData.getSignature();
if (signature == null) if (signature == null)
return 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) if (signature == null)
return false; return false;
// XXX: return this.transaction.getCreator().verify(signature, this.toBytesLessSignature()); return PublicKeyAccount.verify(this.transactionData.getCreatorPublicKey(), signature, this.toBytesLessSignature());
return false;
} }
/** /**
@ -236,30 +249,28 @@ public abstract class Transaction {
* <p> * <p>
* Checks if transaction can have {@link TransactionHandler#process()} called. * Checks if transaction can have {@link TransactionHandler#process()} called.
* <p> * <p>
* Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process()}.
* <p>
* Transactions that have already been processed will return false. * Transactions that have already been processed will return false.
* *
* @return true if transaction can be processed, false otherwise * @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. * Actually process a transaction, updating the blockchain.
* <p> * <p>
* Processes transaction, updating balances, references, assets, etc. as appropriate. * Processes transaction, updating balances, references, assets, etc. as appropriate.
* <p> *
* 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. * Undo transaction, updating the blockchain.
* <p> * <p>
* Undoes transaction, updating balances, references, assets, etc. as appropriate. * Undoes transaction, updating balances, references, assets, etc. as appropriate.
* <p> *
* 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;
} }

View File

@ -6,15 +6,15 @@ import data.account.AccountData;
public interface AccountRepository { public interface AccountRepository {
// General account // General account
public AccountData getAccount(String address) throws DataException; public AccountData getAccount(String address) throws DataException;
public void save(AccountData accountData) throws DataException; public void save(AccountData accountData) throws DataException;
// Account balances // Account balances
public AccountBalanceData getBalance(String address, long assetId) throws DataException; public AccountBalanceData getBalance(String address, long assetId) throws DataException;
public void save(AccountBalanceData accountBalanceData) throws DataException; public void save(AccountBalanceData accountBalanceData) throws DataException;
public void delete(String address, long assetId) throws DataException; public void delete(String address, long assetId) throws DataException;

View File

@ -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;
}

View File

@ -4,6 +4,8 @@ public interface Repository {
public AccountRepository getAccountRepository(); public AccountRepository getAccountRepository();
public AssetRepository getAssetRepository();
public BlockRepository getBlockRepository(); public BlockRepository getBlockRepository();
public TransactionRepository getTransactionRepository(); public TransactionRepository getTransactionRepository();

View File

@ -5,14 +5,14 @@ import data.block.BlockData;
public interface TransactionRepository { 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 int getHeight(TransactionData transactionData);
public BlockData toBlock(TransactionData transactionData); public BlockData toBlock(TransactionData transactionData);
public void save(TransactionData transactionData) throws DataException; public void save(TransactionData transactionData) throws DataException;
public void delete(TransactionData transactionData) throws DataException; public void delete(TransactionData transactionData) throws DataException;

View File

@ -34,7 +34,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
saveHelper.bind("account", accountData.getAddress()).bind("reference", accountData.getReference()); saveHelper.bind("account", accountData.getAddress()).bind("reference", accountData.getReference());
try { try {
saveHelper.execute(this.repository.connection); saveHelper.execute(this.repository);
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException("Unable to save account info into repository", e); throw new DataException("Unable to save account info into repository", e);
} }
@ -60,7 +60,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
accountBalanceData.getBalance()); accountBalanceData.getBalance());
try { try {
saveHelper.execute(this.repository.connection); saveHelper.execute(this.repository);
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException("Unable to save account balance into repository", e); throw new DataException("Unable to save account balance into repository", e);
} }

View File

@ -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);
}
}
}

View File

@ -134,7 +134,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
.bind("AT_data", blockData.getAtBytes()).bind("AT_fees", blockData.getAtFees()); .bind("AT_data", blockData.getAtBytes()).bind("AT_fees", blockData.getAtFees());
try { try {
saveHelper.execute(this.repository.connection); saveHelper.execute(this.repository);
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException("Unable to save Block into repository", 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()); .bind("transaction_signature", blockTransactionData.getTransactionSignature());
try { try {
saveHelper.execute(this.repository.connection); saveHelper.execute(this.repository);
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException("Unable to save BlockTransaction into repository", e); throw new DataException("Unable to save BlockTransaction into repository", e);
} }

View File

@ -9,10 +9,12 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import repository.AccountRepository; import repository.AccountRepository;
import repository.AssetRepository;
import repository.BlockRepository; import repository.BlockRepository;
import repository.DataException; import repository.DataException;
import repository.Repository; import repository.Repository;
import repository.TransactionRepository; import repository.TransactionRepository;
import repository.hsqldb.transaction.HSQLDBTransactionRepository;
public class HSQLDBRepository implements Repository { public class HSQLDBRepository implements Repository {
@ -28,6 +30,11 @@ public class HSQLDBRepository implements Repository {
return new HSQLDBAccountRepository(this); return new HSQLDBAccountRepository(this);
} }
@Override
public AssetRepository getAssetRepository() {
return new HSQLDBAssetRepository(this);
}
@Override @Override
public BlockRepository getBlockRepository() { public BlockRepository getBlockRepository() {
return new HSQLDBBlockRepository(this); return new HSQLDBBlockRepository(this);
@ -79,7 +86,7 @@ public class HSQLDBRepository implements Repository {
* @param inputStream * @param inputStream
* @return byte[] * @return byte[]
*/ */
byte[] getResultSetBytes(InputStream inputStream) { public byte[] getResultSetBytes(InputStream inputStream) {
// inputStream could be null if database's column's value is null // inputStream could be null if database's column's value is null
if (inputStream == null) if (inputStream == null)
return null; return null;
@ -107,7 +114,7 @@ public class HSQLDBRepository implements Repository {
* @return ResultSet, or null if there are no found rows * @return ResultSet, or null if there are no found rows
* @throws SQLException * @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); PreparedStatement preparedStatement = this.connection.prepareStatement(sql);
for (int i = 0; i < objects.length; ++i) 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 * @return ResultSet, or null if there are no found rows
* @throws SQLException * @throws SQLException
*/ */
ResultSet checkedExecute(PreparedStatement preparedStatement) throws SQLException { public ResultSet checkedExecute(PreparedStatement preparedStatement) throws SQLException {
if (!preparedStatement.execute()) if (!preparedStatement.execute())
throw new SQLException("Fetching from database produced no results"); throw new SQLException("Fetching from database produced no results");
@ -154,7 +161,7 @@ public class HSQLDBRepository implements Repository {
* @return Long * @return Long
* @throws SQLException * @throws SQLException
*/ */
Long callIdentity() throws SQLException { public Long callIdentity() throws SQLException {
PreparedStatement preparedStatement = this.connection.prepareStatement("CALL IDENTITY()"); PreparedStatement preparedStatement = this.connection.prepareStatement("CALL IDENTITY()");
ResultSet resultSet = this.checkedExecute(preparedStatement); ResultSet resultSet = this.checkedExecute(preparedStatement);
if (resultSet == null) if (resultSet == null)
@ -180,7 +187,7 @@ public class HSQLDBRepository implements Repository {
* @return true if matching row found in database, false otherwise * @return true if matching row found in database, false otherwise
* @throws SQLException * @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 PreparedStatement preparedStatement = this.connection
.prepareStatement("SELECT TRUE FROM " + tableName + " WHERE " + whereClause + " ORDER BY NULL LIMIT 1"); .prepareStatement("SELECT TRUE FROM " + tableName + " WHERE " + whereClause + " ORDER BY NULL LIMIT 1");
ResultSet resultSet = this.checkedExecute(preparedStatement); ResultSet resultSet = this.checkedExecute(preparedStatement);

View File

@ -1,7 +1,6 @@
package repository.hsqldb; package repository.hsqldb;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
@ -15,7 +14,7 @@ import java.util.List;
* <p> * <p>
* {@code SaveHelper helper = new SaveHelper("TableName"); }<br> * {@code SaveHelper helper = new SaveHelper("TableName"); }<br>
* {@code helper.bind("column_name", someColumnValue).bind("column2", columnValue2); }<br> * {@code helper.bind("column_name", someColumnValue).bind("column2", columnValue2); }<br>
* {@code helper.execute(); }<br> * {@code helper.execute(repository); }<br>
* *
*/ */
public class HSQLDBSaver { public class HSQLDBSaver {
@ -49,14 +48,17 @@ public class HSQLDBSaver {
/** /**
* Build PreparedStatement using bound column-value pairs then execute it. * Build PreparedStatement using bound column-value pairs then execute it.
*
* @param repository
* TODO
* @param repository
* *
* @param connection
* @return the result from {@link PreparedStatement#execute()} * @return the result from {@link PreparedStatement#execute()}
* @throws SQLException * @throws SQLException
*/ */
public boolean execute(Connection connection) throws SQLException { public boolean execute(HSQLDBRepository repository) throws SQLException {
String sql = this.formatInsertWithPlaceholders(); String sql = this.formatInsertWithPlaceholders();
PreparedStatement preparedStatement = connection.prepareStatement(sql); PreparedStatement preparedStatement = repository.connection.prepareStatement(sql);
this.bindValues(preparedStatement); this.bindValues(preparedStatement);

View File

@ -1,4 +1,4 @@
package repository.hsqldb; package repository.hsqldb.transaction;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.sql.ResultSet; import java.sql.ResultSet;
@ -7,6 +7,8 @@ import java.sql.SQLException;
import data.transaction.GenesisTransactionData; import data.transaction.GenesisTransactionData;
import data.transaction.TransactionData; import data.transaction.TransactionData;
import repository.DataException; import repository.DataException;
import repository.hsqldb.HSQLDBRepository;
import repository.hsqldb.HSQLDBSaver;
public class HSQLDBGenesisTransactionRepository extends HSQLDBTransactionRepository { public class HSQLDBGenesisTransactionRepository extends HSQLDBTransactionRepository {
@ -14,7 +16,7 @@ public class HSQLDBGenesisTransactionRepository extends HSQLDBTransactionReposit
super(repository); 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 { try {
ResultSet rs = this.repository.checkedExecute("SELECT recipient, amount FROM GenesisTransactions WHERE signature = ?", signature); ResultSet rs = this.repository.checkedExecute("SELECT recipient, amount FROM GenesisTransactions WHERE signature = ?", signature);
if (rs == null) if (rs == null)
@ -25,22 +27,24 @@ public class HSQLDBGenesisTransactionRepository extends HSQLDBTransactionReposit
return new GenesisTransactionData(recipient, amount, timestamp, signature); return new GenesisTransactionData(recipient, amount, timestamp, signature);
} catch (SQLException e) { } catch (SQLException e) {
return null; throw new DataException("Unable to fetch genesis transaction from repository", e);
} }
} }
@Override @Override
public void save(TransactionData transaction) throws DataException { public void save(TransactionData transactionData) throws DataException {
super.save(transaction); super.save(transactionData);
GenesisTransactionData genesisTransactionData = (GenesisTransactionData) transactionData;
GenesisTransactionData genesisTransaction = (GenesisTransactionData) transaction;
HSQLDBSaver saveHelper = new HSQLDBSaver("GenesisTransactions"); 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 { try {
saveHelper.execute(this.repository.connection); saveHelper.execute(this.repository);
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException(e); throw new DataException("Unable to save genesis transaction into repository", e);
} }
} }

View File

@ -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);
}
}
}

View File

@ -1,4 +1,4 @@
package repository.hsqldb; package repository.hsqldb.transaction;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.sql.ResultSet; import java.sql.ResultSet;
@ -10,18 +10,22 @@ import data.transaction.TransactionData;
import qora.transaction.Transaction.TransactionType; import qora.transaction.Transaction.TransactionType;
import repository.DataException; import repository.DataException;
import repository.TransactionRepository; import repository.TransactionRepository;
import repository.hsqldb.HSQLDBRepository;
import repository.hsqldb.HSQLDBSaver;
public class HSQLDBTransactionRepository implements TransactionRepository { public class HSQLDBTransactionRepository implements TransactionRepository {
protected HSQLDBRepository repository; protected HSQLDBRepository repository;
private HSQLDBGenesisTransactionRepository genesisTransactionRepository; private HSQLDBGenesisTransactionRepository genesisTransactionRepository;
private HSQLDBIssueAssetTransactionRepository issueAssetTransactionRepository;
public HSQLDBTransactionRepository(HSQLDBRepository repository) { public HSQLDBTransactionRepository(HSQLDBRepository repository) {
this.repository = repository; this.repository = repository;
genesisTransactionRepository = new HSQLDBGenesisTransactionRepository(repository); genesisTransactionRepository = new HSQLDBGenesisTransactionRepository(repository);
issueAssetTransactionRepository = new HSQLDBIssueAssetTransactionRepository(repository);
} }
public TransactionData fromSignature(byte[] signature) { public TransactionData fromSignature(byte[] signature) throws DataException {
try { try {
ResultSet rs = this.repository.checkedExecute("SELECT type, reference, creator, creation, fee FROM Transactions WHERE signature = ?", signature); ResultSet rs = this.repository.checkedExecute("SELECT type, reference, creator, creation, fee FROM Transactions WHERE signature = ?", signature);
if (rs == null) if (rs == null)
@ -35,11 +39,11 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
return this.fromBase(type, signature, reference, creator, timestamp, fee); return this.fromBase(type, signature, reference, creator, timestamp, fee);
} catch (SQLException e) { } 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 { try {
ResultSet rs = this.repository.checkedExecute("SELECT type, signature, creator, creation, fee FROM Transactions WHERE reference = ?", reference); ResultSet rs = this.repository.checkedExecute("SELECT type, signature, creator, creation, fee FROM Transactions WHERE reference = ?", reference);
if (rs == null) if (rs == null)
@ -53,15 +57,19 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
return this.fromBase(type, signature, reference, creator, timestamp, fee); return this.fromBase(type, signature, reference, creator, timestamp, fee);
} catch (SQLException e) { } 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) { switch (type) {
case GENESIS: case GENESIS:
return this.genesisTransactionRepository.fromBase(signature, reference, creator, timestamp, fee); return this.genesisTransactionRepository.fromBase(signature, reference, creator, timestamp, fee);
case ISSUE_ASSET:
return this.issueAssetTransactionRepository.fromBase(signature, reference, creator, timestamp, fee);
default: default:
return null; return null;
} }
@ -115,7 +123,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
.bind("creator", transactionData.getCreatorPublicKey()).bind("creation", new Timestamp(transactionData.getTimestamp())) .bind("creator", transactionData.getCreatorPublicKey()).bind("creation", new Timestamp(transactionData.getTimestamp()))
.bind("fee", transactionData.getFee()).bind("milestone_block", null); .bind("fee", transactionData.getFee()).bind("milestone_block", null);
try { try {
saver.execute(this.repository.connection); saver.execute(this.repository);
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException(e); throw new DataException(e);
} }

View File

@ -2,6 +2,7 @@ package transform;
public abstract class Transformer { public abstract class Transformer {
public static final int BOOLEAN_LENGTH = 4;
public static final int INT_LENGTH = 4; public static final int INT_LENGTH = 4;
public static final int LONG_LENGTH = 8; 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 ADDRESS_LENGTH = 25;
public static final int PUBLIC_KEY_LENGTH = 32; 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; public static final int TIMESTAMP_LENGTH = LONG_LENGTH;
} }

View File

@ -35,20 +35,20 @@ public class GenesisTransactionTransformer extends TransactionTransformer {
return new GenesisTransactionData(recipient, amount, timestamp); 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; return TYPE_LENGTH + TYPELESS_LENGTH;
} }
public static byte[] toBytes(TransactionData baseTransaction) throws TransformationException { public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
try { try {
GenesisTransactionData transaction = (GenesisTransactionData) baseTransaction; GenesisTransactionData genesisTransactionData = (GenesisTransactionData) transactionData;
ByteArrayOutputStream bytes = new ByteArrayOutputStream(); ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(transaction.getType().value)); bytes.write(Ints.toByteArray(genesisTransactionData.getType().value));
bytes.write(Longs.toByteArray(transaction.getTimestamp())); bytes.write(Longs.toByteArray(genesisTransactionData.getTimestamp()));
bytes.write(Base58.decode(transaction.getRecipient())); bytes.write(Base58.decode(genesisTransactionData.getRecipient()));
bytes.write(Serialization.serializeBigDecimal(transaction.getAmount())); bytes.write(Serialization.serializeBigDecimal(genesisTransactionData.getAmount()));
return bytes.toByteArray(); return bytes.toByteArray();
} catch (IOException | ClassCastException e) { } catch (IOException | ClassCastException e) {
@ -57,14 +57,14 @@ public class GenesisTransactionTransformer extends TransactionTransformer {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static JSONObject toJSON(TransactionData baseTransaction) throws TransformationException { public static JSONObject toJSON(TransactionData transactionData) throws TransformationException {
JSONObject json = TransactionTransformer.getBaseJSON(baseTransaction); JSONObject json = TransactionTransformer.getBaseJSON(transactionData);
try { try {
GenesisTransactionData transaction = (GenesisTransactionData) baseTransaction; GenesisTransactionData genesisTransactionData = (GenesisTransactionData) transactionData;
json.put("recipient", transaction.getRecipient()); json.put("recipient", genesisTransactionData.getRecipient());
json.put("amount", transaction.getAmount().toPlainString()); json.put("amount", genesisTransactionData.getAmount().toPlainString());
} catch (ClassCastException e) { } catch (ClassCastException e) {
throw new TransformationException(e); throw new TransformationException(e);
} }

View File

@ -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;
}
}

View File

@ -13,6 +13,8 @@ import utils.Base58;
public class TransactionTransformer extends Transformer { public class TransactionTransformer extends Transformer {
protected static final int TYPE_LENGTH = INT_LENGTH; 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 { public static TransactionData fromBytes(byte[] bytes) throws TransformationException {
if (bytes == null) if (bytes == null)
@ -31,25 +33,34 @@ public class TransactionTransformer extends Transformer {
case GENESIS: case GENESIS:
return GenesisTransactionTransformer.fromByteBuffer(byteBuffer); return GenesisTransactionTransformer.fromByteBuffer(byteBuffer);
case ISSUE_ASSET:
return IssueAssetTransactionTransformer.fromByteBuffer(byteBuffer);
default: default:
return null; return null;
} }
} }
public static int getDataLength(TransactionData transaction) throws TransformationException { public static int getDataLength(TransactionData transactionData) throws TransformationException {
switch (transaction.getType()) { switch (transactionData.getType()) {
case GENESIS: case GENESIS:
return GenesisTransactionTransformer.getDataLength(transaction); return GenesisTransactionTransformer.getDataLength(transactionData);
case ISSUE_ASSET:
return IssueAssetTransactionTransformer.getDataLength(transactionData);
default: default:
throw new TransformationException("Unsupported transaction type"); throw new TransformationException("Unsupported transaction type");
} }
} }
public static byte[] toBytes(TransactionData transaction) throws TransformationException { public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
switch (transaction.getType()) { switch (transactionData.getType()) {
case GENESIS: case GENESIS:
return GenesisTransactionTransformer.toBytes(transaction); return GenesisTransactionTransformer.toBytes(transactionData);
case ISSUE_ASSET:
return IssueAssetTransactionTransformer.toBytes(transactionData);
default: default:
return null; return null;

View File

@ -1,10 +1,14 @@
package utils; package utils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import com.google.common.primitives.Ints;
import transform.TransformationException; import transform.TransformationException;
import transform.Transformer; import transform.Transformer;
@ -41,6 +45,11 @@ public class Serialization {
return bytes; 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 { public static String deserializeSizedString(ByteBuffer byteBuffer, int maxSize) throws TransformationException {
int size = byteBuffer.getInt(); int size = byteBuffer.getInt();
if (size > maxSize || size > byteBuffer.remaining()) if (size > maxSize || size > byteBuffer.remaining())