forked from Qortal/qortal
Converted Message Transactions
This commit is contained in:
parent
16a92305d2
commit
f2d7a3d0cd
74
src/data/transaction/MessageTransactionData.java
Normal file
74
src/data/transaction/MessageTransactionData.java
Normal file
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
162
src/transform/transaction/MessageTransactionTransformer.java
Normal file
162
src/transform/transaction/MessageTransactionTransformer.java
Normal file
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user