diff --git a/src/data/transaction/MessageTransactionData.java b/src/data/transaction/MessageTransactionData.java new file mode 100644 index 00000000..e20060ed --- /dev/null +++ b/src/data/transaction/MessageTransactionData.java @@ -0,0 +1,74 @@ +package data.transaction; + +import java.math.BigDecimal; + +import qora.assets.Asset; +import qora.transaction.Transaction.TransactionType; + +public class MessageTransactionData extends TransactionData { + + // Properties + protected int version; + protected byte[] senderPublicKey; + protected String recipient; + protected Long assetId; + protected BigDecimal amount; + protected byte[] data; + protected boolean isText; + protected boolean isEncrypted; + + // Constructors + public MessageTransactionData(int version, byte[] senderPublicKey, String recipient, Long assetId, BigDecimal amount, BigDecimal fee, byte[] data, + boolean isText, boolean isEncrypted, long timestamp, byte[] reference, byte[] signature) { + super(TransactionType.MESSAGE, fee, senderPublicKey, timestamp, reference, signature); + + this.version = version; + this.senderPublicKey = senderPublicKey; + this.recipient = recipient; + + if (assetId != null) + this.assetId = assetId; + else + this.assetId = Asset.QORA; + + this.amount = amount; + this.data = data; + this.isText = isText; + this.isEncrypted = isEncrypted; + } + + // Getters/Setters + + public int getVersion() { + return this.version; + } + + public byte[] getSenderPublicKey() { + return this.senderPublicKey; + } + + public String getRecipient() { + return this.recipient; + } + + public Long getAssetId() { + return this.assetId; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public byte[] getData() { + return this.data; + } + + public boolean getIsText() { + return this.isText; + } + + public boolean getIsEncrypted() { + return this.isEncrypted; + } + +} diff --git a/src/qora/transaction/MessageTransaction.java b/src/qora/transaction/MessageTransaction.java index c1ece0c1..41069261 100644 --- a/src/qora/transaction/MessageTransaction.java +++ b/src/qora/transaction/MessageTransaction.java @@ -1,375 +1,142 @@ package qora.transaction; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.math.BigDecimal; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -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.transaction.MessageTransactionData; +import data.transaction.TransactionData; import qora.account.Account; import qora.account.PublicKeyAccount; import qora.assets.Asset; import qora.block.Block; -import qora.block.BlockChain; import qora.crypto.Crypto; -import repository.hsqldb.HSQLDBSaver; -import transform.TransformationException; -import utils.Base58; -import utils.Serialization; +import repository.DataException; +import repository.Repository; -public class MessageTransaction extends TransactionHandler { +public class MessageTransaction extends Transaction { - // Properties - protected int version; - protected PublicKeyAccount sender; - protected Account recipient; - protected Long assetId; - protected BigDecimal amount; - protected byte[] data; - protected boolean isText; - protected boolean isEncrypted; - - // Property lengths - private static final int SENDER_LENGTH = 32; - private static final int AMOUNT_LENGTH = 8; - private static final int ASSET_ID_LENGTH = 8; - private static final int DATA_SIZE_LENGTH = 4; - private static final int IS_TEXT_LENGTH = 1; - private static final int IS_ENCRYPTED_LENGTH = 1; - private static final int TYPELESS_DATALESS_LENGTH_V1 = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH + DATA_SIZE_LENGTH - + IS_TEXT_LENGTH + IS_ENCRYPTED_LENGTH; - private static final int TYPELESS_DATALESS_LENGTH_V3 = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + ASSET_ID_LENGTH + AMOUNT_LENGTH - + DATA_SIZE_LENGTH + IS_TEXT_LENGTH + IS_ENCRYPTED_LENGTH; - - // Other property lengths private static final int MAX_DATA_SIZE = 4000; // Constructors - public MessageTransaction(PublicKeyAccount sender, String recipient, Long assetId, BigDecimal amount, BigDecimal fee, byte[] data, boolean isText, - boolean isEncrypted, long timestamp, byte[] reference, byte[] signature) { - super(TransactionType.MESSAGE, fee, sender, timestamp, reference, signature); - - this.version = TransactionHandler.getVersionByTimestamp(this.timestamp); - this.sender = sender; - this.recipient = new Account(recipient); - - if (assetId != null) - this.assetId = assetId; - else - this.assetId = Asset.QORA; - - this.amount = amount; - this.data = data; - this.isText = isText; - this.isEncrypted = isEncrypted; - } - - // Getters/Setters - - public int getVersion() { - return this.version; - } - - public Account getSender() { - return this.sender; - } - - public Account getRecipient() { - return this.recipient; - } - - public Long getAssetId() { - return this.assetId; - } - - public BigDecimal getAmount() { - return this.amount; - } - - public byte[] getData() { - return this.data; - } - - public boolean isText() { - return this.isText; - } - - public boolean isEncrypted() { - return this.isEncrypted; - } - - // More information - - public int getDataLength() { - if (this.version == 1) - return TYPE_LENGTH + TYPELESS_DATALESS_LENGTH_V1 + this.data.length; - else - return TYPE_LENGTH + TYPELESS_DATALESS_LENGTH_V3 + this.data.length; - } - - // Load/Save - - /** - * Construct MessageTransaction from DB using signature. - * - * @param signature - * @throws NoDataFoundException - * if no matching row found - * @throws SQLException - */ - protected MessageTransaction(byte[] signature) throws SQLException { - super(TransactionType.MESSAGE, signature); - - ResultSet rs = DB.checkedExecute( - "SELECT version, sender, recipient, is_text, is_encrypted, amount, asset_id, data FROM MessageTransactions WHERE signature = ?", signature); - if (rs == null) - throw new NoDataFoundException(); - - this.version = rs.getInt(1); - this.sender = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(2), CREATOR_LENGTH)); - this.recipient = new Account(rs.getString(3)); - this.isText = rs.getBoolean(4); - this.isEncrypted = rs.getBoolean(5); - this.amount = rs.getBigDecimal(6).setScale(8); - this.assetId = rs.getLong(7); - this.data = DB.getResultSetBytes(rs.getBinaryStream(8)); - } - - /** - * Load MessageTransaction from DB using signature. - * - * @param signature - * @return MessageTransaction, or null if not found - * @throws SQLException - */ - public static MessageTransaction fromSignature(byte[] signature) throws SQLException { - try { - return new MessageTransaction(signature); - } catch (NoDataFoundException e) { - return null; - } - } - - @Override - public void save() throws SQLException { - super.save(); - - HSQLDBSaver saveHelper = new HSQLDBSaver("MessageTransactions"); - saveHelper.bind("signature", this.signature).bind("version", this.version).bind("sender", this.sender.getPublicKey()) - .bind("recipient", this.recipient.getAddress()).bind("is_text", this.isText).bind("is_encrypted", this.isEncrypted).bind("amount", this.amount) - .bind("asset_id", this.assetId).bind("data", this.data); - saveHelper.execute(); - } - - // Converters - - protected static TransactionHandler parse(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TIMESTAMP_LENGTH) - throw new TransformationException("Byte data too short for MessageTransaction"); - - long timestamp = byteBuffer.getLong(); - int version = TransactionHandler.getVersionByTimestamp(timestamp); - - int minimumRemaining = version == 1 ? TYPELESS_DATALESS_LENGTH_V1 : TYPELESS_DATALESS_LENGTH_V3; - minimumRemaining -= TIMESTAMP_LENGTH; // Already read above - - if (byteBuffer.remaining() < minimumRemaining) - throw new TransformationException("Byte data too short for MessageTransaction"); - - byte[] reference = new byte[REFERENCE_LENGTH]; - byteBuffer.get(reference); - - PublicKeyAccount sender = Serialization.deserializePublicKey(byteBuffer); - String recipient = Serialization.deserializeRecipient(byteBuffer); - - long assetId; - if (version == 1) - assetId = Asset.QORA; - else - assetId = byteBuffer.getLong(); - - BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); - - int dataSize = byteBuffer.getInt(0); - // Don't allow invalid dataSize here to avoid run-time issues - if (dataSize > MAX_DATA_SIZE) - throw new TransformationException("MessageTransaction data size too large"); - - byte[] data = new byte[dataSize]; - byteBuffer.get(data); - - boolean isEncrypted = byteBuffer.get() != 0; - boolean isText = byteBuffer.get() != 0; - - BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); - - byte[] signature = new byte[SIGNATURE_LENGTH]; - byteBuffer.get(signature); - - return new MessageTransaction(sender, recipient, assetId, amount, fee, data, isText, isEncrypted, timestamp, reference, signature); - } - - @SuppressWarnings("unchecked") - @Override - public JSONObject toJSON() throws SQLException { - JSONObject json = getBaseJSON(); - - json.put("version", this.version); - json.put("sender", this.sender.getAddress()); - json.put("senderPublicKey", HashCode.fromBytes(this.sender.getPublicKey()).toString()); - json.put("recipient", this.recipient.getAddress()); - json.put("amount", this.amount.toPlainString()); - json.put("assetId", this.assetId); - json.put("isText", this.isText); - json.put("isEncrypted", this.isEncrypted); - - // We can only show plain text as unencoded - if (this.isText && !this.isEncrypted) - json.put("data", new String(this.data, Charset.forName("UTF-8"))); - else - json.put("data", HashCode.fromBytes(this.data).toString()); - - 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.sender.getPublicKey()); - bytes.write(Base58.decode(this.recipient.getAddress())); - - if (this.version != 1) - bytes.write(Longs.toByteArray(this.assetId)); - - bytes.write(Serialization.serializeBigDecimal(this.amount)); - - bytes.write(Ints.toByteArray(this.data.length)); - bytes.write(this.data); - - bytes.write((byte) (this.isEncrypted ? 1 : 0)); - bytes.write((byte) (this.isText ? 1 : 0)); - - bytes.write(Serialization.serializeBigDecimal(this.fee)); - bytes.write(this.signature); - return bytes.toByteArray(); - } catch (IOException e) { - throw new RuntimeException(e); - } + public MessageTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); } // Processing - public ValidationResult isValid() throws SQLException { + public ValidationResult isValid() throws DataException { // Lowest cost checks first + MessageTransactionData messageTransactionData = (MessageTransactionData) this.transactionData; + // Are message transactions even allowed at this point? - if (this.version != TransactionHandler.getVersionByTimestamp(this.timestamp)) + if (messageTransactionData.getVersion() != MessageTransaction.getVersionByTimestamp(messageTransactionData.getTimestamp())) return ValidationResult.NOT_YET_RELEASED; - if (BlockChain.getHeight() < Block.MESSAGE_RELEASE_HEIGHT) + if (this.repository.getBlockRepository().getBlockchainHeight() < Block.MESSAGE_RELEASE_HEIGHT) return ValidationResult.NOT_YET_RELEASED; // Check data length - if (this.data.length < 1 || this.data.length > MAX_DATA_SIZE) + if (messageTransactionData.getData().length < 1 || messageTransactionData.getData().length > MAX_DATA_SIZE) return ValidationResult.INVALID_DATA_LENGTH; // Check recipient is a valid address - if (!Crypto.isValidAddress(this.recipient.getAddress())) + if (!Crypto.isValidAddress(messageTransactionData.getRecipient())) return ValidationResult.INVALID_ADDRESS; - if (this.version == 1) { + if (messageTransactionData.getVersion() == 1) { // Check amount is positive (V1) - if (this.amount.compareTo(BigDecimal.ZERO) <= 0) + if (messageTransactionData.getAmount().compareTo(BigDecimal.ZERO) <= 0) return ValidationResult.NEGATIVE_AMOUNT; } else { // Check amount is not negative (V3) as sending messages without a payment is OK - if (this.amount.compareTo(BigDecimal.ZERO) < 0) + if (messageTransactionData.getAmount().compareTo(BigDecimal.ZERO) < 0) return ValidationResult.NEGATIVE_AMOUNT; } // Check fee is positive - if (this.fee.compareTo(BigDecimal.ZERO) <= 0) + if (messageTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) return ValidationResult.NEGATIVE_FEE; // Check reference is correct - if (!Arrays.equals(this.sender.getLastReference(), this.reference)) + Account sender = new PublicKeyAccount(this.repository, messageTransactionData.getSenderPublicKey()); + if (!Arrays.equals(sender.getLastReference(), messageTransactionData.getReference())) return ValidationResult.INVALID_REFERENCE; // Does asset exist? (This test not present in gen1) - if (this.assetId != Asset.QORA && !Asset.exists(this.assetId)) + long assetId = messageTransactionData.getAssetId(); + if (assetId != Asset.QORA && !this.repository.getAssetRepository().assetExists(assetId)) return ValidationResult.ASSET_DOES_NOT_EXIST; // If asset is QORA then we need to check amount + fee in one go - if (this.assetId == Asset.QORA) { + if (assetId == Asset.QORA) { // Check sender has enough funds for amount + fee in QORA - if (this.sender.getConfirmedBalance(Asset.QORA).compareTo(this.amount.add(this.fee)) == -1) + if (sender.getConfirmedBalance(Asset.QORA).compareTo(messageTransactionData.getAmount().add(messageTransactionData.getFee())) == -1) return ValidationResult.NO_BALANCE; } else { // Check sender has enough funds for amount in whatever asset - if (this.sender.getConfirmedBalance(this.assetId).compareTo(this.amount) == -1) + if (sender.getConfirmedBalance(assetId).compareTo(messageTransactionData.getAmount()) == -1) return ValidationResult.NO_BALANCE; // Check sender has enough funds for fee in QORA - if (this.sender.getConfirmedBalance(Asset.QORA).compareTo(this.fee) == -1) + if (sender.getConfirmedBalance(Asset.QORA).compareTo(messageTransactionData.getFee()) == -1) return ValidationResult.NO_BALANCE; } return ValidationResult.OK; } - public void process() throws SQLException { - this.save(); + public void process() throws DataException { + MessageTransactionData messageTransactionData = (MessageTransactionData) this.transactionData; + long assetId = messageTransactionData.getAssetId(); + + // Save this transaction itself + this.repository.getTransactionRepository().save(this.transactionData); // Update sender's balance due to amount - this.sender.setConfirmedBalance(this.assetId, this.sender.getConfirmedBalance(this.assetId).subtract(this.amount)); + Account sender = new PublicKeyAccount(this.repository, messageTransactionData.getSenderPublicKey()); + sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(messageTransactionData.getAmount())); // Update sender's balance due to fee - this.sender.setConfirmedBalance(Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).subtract(this.fee)); + sender.setConfirmedBalance(Asset.QORA, sender.getConfirmedBalance(Asset.QORA).subtract(messageTransactionData.getFee())); // Update recipient's balance - this.recipient.setConfirmedBalance(this.assetId, this.recipient.getConfirmedBalance(this.assetId).add(this.amount)); + Account recipient = new Account(this.repository, messageTransactionData.getRecipient()); + recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(messageTransactionData.getAmount())); // Update sender's reference - this.sender.setLastReference(this.signature); + sender.setLastReference(messageTransactionData.getSignature()); // For QORA amounts only: if recipient has no reference yet, then this is their starting reference - if (this.assetId == Asset.QORA && this.recipient.getLastReference() == null) - this.recipient.setLastReference(this.signature); + if (assetId == Asset.QORA && recipient.getLastReference() == null) + recipient.setLastReference(messageTransactionData.getSignature()); } - public void orphan() throws SQLException { - this.delete(); + public void orphan() throws DataException { + MessageTransactionData messageTransactionData = (MessageTransactionData) this.transactionData; + long assetId = messageTransactionData.getAssetId(); + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(this.transactionData); // Update sender's balance due to amount - this.sender.setConfirmedBalance(this.assetId, this.sender.getConfirmedBalance(this.assetId).add(this.amount)); + Account sender = new PublicKeyAccount(this.repository, messageTransactionData.getSenderPublicKey()); + sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(messageTransactionData.getAmount())); // Update sender's balance due to fee - this.sender.setConfirmedBalance(Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).add(this.fee)); + sender.setConfirmedBalance(Asset.QORA, sender.getConfirmedBalance(Asset.QORA).add(messageTransactionData.getFee())); // Update recipient's balance - this.recipient.setConfirmedBalance(this.assetId, this.recipient.getConfirmedBalance(this.assetId).subtract(this.amount)); + Account recipient = new Account(this.repository, messageTransactionData.getRecipient()); + recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(messageTransactionData.getAmount())); // Update sender's reference - this.sender.setLastReference(this.reference); + sender.setLastReference(messageTransactionData.getReference()); /* * For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own (which * would have changed their last reference) thus this is their first reference so remove it. */ - if (this.assetId == Asset.QORA && Arrays.equals(this.recipient.getLastReference(), this.signature)) - this.recipient.setLastReference(null); + if (assetId == Asset.QORA && Arrays.equals(recipient.getLastReference(), messageTransactionData.getSignature())) + recipient.setLastReference(null); } } diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index dfc95047..5f836d2d 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -89,6 +89,9 @@ public abstract class Transaction { case CREATE_ASSET_ORDER: return new CreateOrderTransaction(repository, transactionData); + case MESSAGE: + return new MessageTransaction(repository, transactionData); + default: return null; } diff --git a/src/repository/hsqldb/transaction/HSQLDBMessageTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBMessageTransactionRepository.java new file mode 100644 index 00000000..35b76321 --- /dev/null +++ b/src/repository/hsqldb/transaction/HSQLDBMessageTransactionRepository.java @@ -0,0 +1,63 @@ +package repository.hsqldb.transaction; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import data.transaction.MessageTransactionData; +import data.transaction.TransactionData; +import repository.DataException; +import repository.hsqldb.HSQLDBRepository; +import repository.hsqldb.HSQLDBSaver; + +public class HSQLDBMessageTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBMessageTransactionRepository(HSQLDBRepository repository) { + super(repository); + } + + TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException { + try { + ResultSet rs = this.repository.checkedExecute( + "SELECT version, sender, recipient, is_text, is_encrypted, amount, asset_id, data FROM MessageTransactions WHERE signature = ?", signature); + if (rs == null) + return null; + + int version = rs.getInt(1); + byte[] senderPublicKey = this.repository.getResultSetBytes(rs.getBinaryStream(2)); + String recipient = rs.getString(3); + boolean isText = rs.getBoolean(4); + boolean isEncrypted = rs.getBoolean(5); + BigDecimal amount = rs.getBigDecimal(6); + Long assetId = rs.getLong(7); + byte[] data = this.repository.getResultSetBytes(rs.getBinaryStream(8)); + + return new MessageTransactionData(version, senderPublicKey, recipient, assetId, amount, fee, data, isText, isEncrypted, timestamp, reference, + signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch message transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + super.save(transactionData); + + MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("MessageTransactions"); + + saveHelper.bind("signature", messageTransactionData.getSignature()).bind("version", messageTransactionData.getVersion()) + .bind("sender", messageTransactionData.getSenderPublicKey()).bind("recipient", messageTransactionData.getRecipient()) + .bind("is_text", messageTransactionData.getIsText()).bind("is_encrypted", messageTransactionData.getIsEncrypted()) + .bind("amount", messageTransactionData.getAmount()).bind("asset_id", messageTransactionData.getAssetId()) + .bind("data", messageTransactionData.getData()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save message transaction into repository", e); + } + } + +} diff --git a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index 12fff172..ab0402f2 100644 --- a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -20,6 +20,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { private HSQLDBPaymentTransactionRepository paymentTransactionRepository; private HSQLDBIssueAssetTransactionRepository issueAssetTransactionRepository; private HSQLDBCreateOrderTransactionRepository createOrderTransactionRepository; + private HSQLDBMessageTransactionRepository messageTransactionRepository; public HSQLDBTransactionRepository(HSQLDBRepository repository) { this.repository = repository; @@ -27,6 +28,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.paymentTransactionRepository = new HSQLDBPaymentTransactionRepository(repository); this.issueAssetTransactionRepository = new HSQLDBIssueAssetTransactionRepository(repository); this.createOrderTransactionRepository = new HSQLDBCreateOrderTransactionRepository(repository); + this.messageTransactionRepository = new HSQLDBMessageTransactionRepository(repository); } public TransactionData fromSignature(byte[] signature) throws DataException { @@ -80,6 +82,9 @@ public class HSQLDBTransactionRepository implements TransactionRepository { case CREATE_ASSET_ORDER: return this.createOrderTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case MESSAGE: + return this.messageTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + default: return null; } diff --git a/src/transform/transaction/MessageTransactionTransformer.java b/src/transform/transaction/MessageTransactionTransformer.java new file mode 100644 index 00000000..a3f48442 --- /dev/null +++ b/src/transform/transaction/MessageTransactionTransformer.java @@ -0,0 +1,162 @@ +package transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +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 qora.assets.Asset; +import qora.transaction.MessageTransaction; +import data.transaction.MessageTransactionData; +import transform.TransformationException; +import utils.Base58; +import utils.Serialization; + +public class MessageTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int SENDER_LENGTH = PUBLIC_KEY_LENGTH; + private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH; + private static final int AMOUNT_LENGTH = 8; + private static final int ASSET_ID_LENGTH = LONG_LENGTH; + private static final int DATA_SIZE_LENGTH = INT_LENGTH; + private static final int IS_TEXT_LENGTH = BOOLEAN_LENGTH; + private static final int IS_ENCRYPTED_LENGTH = BOOLEAN_LENGTH; + + private static final int TYPELESS_DATALESS_LENGTH_V1 = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH + DATA_SIZE_LENGTH + + IS_TEXT_LENGTH + IS_ENCRYPTED_LENGTH; + private static final int TYPELESS_DATALESS_LENGTH_V3 = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + ASSET_ID_LENGTH + AMOUNT_LENGTH + + DATA_SIZE_LENGTH + IS_TEXT_LENGTH + IS_ENCRYPTED_LENGTH; + + // Other property lengths + private static final int MAX_DATA_SIZE = 4000; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + if (byteBuffer.remaining() < TYPELESS_DATALESS_LENGTH_V1) + throw new TransformationException("Byte data too short for MessageTransaction"); + + long timestamp = byteBuffer.getLong(); + int version = MessageTransaction.getVersionByTimestamp(timestamp); + + int minimumRemaining = version == 1 ? TYPELESS_DATALESS_LENGTH_V1 : TYPELESS_DATALESS_LENGTH_V3; + minimumRemaining -= TIMESTAMP_LENGTH; // Already read above + + if (byteBuffer.remaining() < minimumRemaining) + throw new TransformationException("Byte data too short for MessageTransaction"); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer); + String recipient = Serialization.deserializeRecipient(byteBuffer); + + long assetId; + if (version == 1) + assetId = Asset.QORA; + else + assetId = byteBuffer.getLong(); + + BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); + + int dataSize = byteBuffer.getInt(0); + // Don't allow invalid dataSize here to avoid run-time issues + if (dataSize > MAX_DATA_SIZE) + throw new TransformationException("MessageTransaction data size too large"); + + byte[] data = new byte[dataSize]; + byteBuffer.get(data); + + boolean isEncrypted = byteBuffer.get() != 0; + boolean isText = byteBuffer.get() != 0; + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new MessageTransactionData(version, senderPublicKey, recipient, assetId, amount, fee, data, isText, isEncrypted, timestamp, reference, + signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData; + + if (messageTransactionData.getVersion() == 1) + return TYPE_LENGTH + TYPELESS_DATALESS_LENGTH_V1 + messageTransactionData.getData().length; + else + return TYPE_LENGTH + TYPELESS_DATALESS_LENGTH_V3 + messageTransactionData.getData().length; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(messageTransactionData.getType().value)); + bytes.write(Longs.toByteArray(messageTransactionData.getTimestamp())); + bytes.write(messageTransactionData.getReference()); + + bytes.write(messageTransactionData.getSenderPublicKey()); + bytes.write(Base58.decode(messageTransactionData.getRecipient())); + + if (messageTransactionData.getVersion() != 1) + bytes.write(Longs.toByteArray(messageTransactionData.getAssetId())); + + bytes.write(Serialization.serializeBigDecimal(messageTransactionData.getAmount())); + + bytes.write(Ints.toByteArray(messageTransactionData.getData().length)); + bytes.write(messageTransactionData.getData()); + + bytes.write((byte) (messageTransactionData.getIsEncrypted() ? 1 : 0)); + bytes.write((byte) (messageTransactionData.getIsText() ? 1 : 0)); + + bytes.write(Serialization.serializeBigDecimal(messageTransactionData.getFee())); + bytes.write(messageTransactionData.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 { + MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData; + + byte[] senderPublicKey = messageTransactionData.getSenderPublicKey(); + + json.put("version", messageTransactionData.getVersion()); + json.put("sender", PublicKeyAccount.getAddress(senderPublicKey)); + json.put("senderPublicKey", HashCode.fromBytes(senderPublicKey).toString()); + json.put("recipient", messageTransactionData.getRecipient()); + json.put("amount", messageTransactionData.getAmount().toPlainString()); + json.put("assetId", messageTransactionData.getAssetId()); + json.put("isText", messageTransactionData.getIsText()); + json.put("isEncrypted", messageTransactionData.getIsEncrypted()); + + // We can only show plain text as unencoded + if (messageTransactionData.getIsText() && !messageTransactionData.getIsEncrypted()) + json.put("data", new String(messageTransactionData.getData(), Charset.forName("UTF-8"))); + else + json.put("data", HashCode.fromBytes(messageTransactionData.getData()).toString()); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/transform/transaction/TransactionTransformer.java b/src/transform/transaction/TransactionTransformer.java index e8d6f5a7..b467614d 100644 --- a/src/transform/transaction/TransactionTransformer.java +++ b/src/transform/transaction/TransactionTransformer.java @@ -42,6 +42,9 @@ public class TransactionTransformer extends Transformer { case CREATE_ASSET_ORDER: return CreateOrderTransactionTransformer.fromByteBuffer(byteBuffer); + case MESSAGE: + return MessageTransactionTransformer.fromByteBuffer(byteBuffer); + default: throw new TransformationException("Unsupported transaction type"); } @@ -61,6 +64,9 @@ public class TransactionTransformer extends Transformer { case CREATE_ASSET_ORDER: return CreateOrderTransactionTransformer.getDataLength(transactionData); + case MESSAGE: + return MessageTransactionTransformer.getDataLength(transactionData); + default: throw new TransformationException("Unsupported transaction type"); } @@ -80,6 +86,9 @@ public class TransactionTransformer extends Transformer { case CREATE_ASSET_ORDER: return CreateOrderTransactionTransformer.toBytes(transactionData); + case MESSAGE: + return MessageTransactionTransformer.toBytes(transactionData); + default: throw new TransformationException("Unsupported transaction type"); } @@ -99,6 +108,9 @@ public class TransactionTransformer extends Transformer { case CREATE_ASSET_ORDER: return CreateOrderTransactionTransformer.toJSON(transaction); + case MESSAGE: + return MessageTransactionTransformer.toJSON(transaction); + default: throw new TransformationException("Unsupported transaction type"); }