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

View File

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

View File

@ -206,7 +206,7 @@ public class Block {
this.transactions = new ArrayList<Transaction>();
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);

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {
* <p>
* Checks if transaction can have {@link TransactionHandler#process()} called.
* <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.
*
* @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.
* <p>
* 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.
* <p>
* 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 {
// 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;

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 AssetRepository getAssetRepository();
public BlockRepository getBlockRepository();
public TransactionRepository getTransactionRepository();

View File

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

View File

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

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

View File

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

View File

@ -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;
* <p>
* {@code SaveHelper helper = new SaveHelper("TableName"); }<br>
* {@code helper.bind("column_name", someColumnValue).bind("column2", columnValue2); }<br>
* {@code helper.execute(); }<br>
* {@code helper.execute(repository); }<br>
*
*/
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);

View File

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

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

View File

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

View File

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

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 {
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;

View File

@ -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())