qortal/src/qora/transaction/MessageTransaction.java
catbref 5b78268915 Convertion to thread-local Connection
Much tidier code thanks to not having to pass Connection objects around
as params. Also no need for two forms of the same method, one with Connection
param, one without.

Also corrected SQL-Transaction-related methods in DB, e.g. commit, rollback, etc.
so they use the proper underlying JDBC methods.
2018-06-06 11:39:58 +01:00

376 lines
12 KiB
Java

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 database.SaveHelper;
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 utils.Base58;
import utils.ParseException;
import utils.Serialization;
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 = Transaction.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();
SaveHelper saveHelper = new SaveHelper("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 Transaction parse(ByteBuffer byteBuffer) throws ParseException {
if (byteBuffer.remaining() < TIMESTAMP_LENGTH)
throw new ParseException("Byte data too short for MessageTransaction");
long timestamp = byteBuffer.getLong();
int version = Transaction.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 ParseException("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 ParseException("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);
}
}
// Processing
public ValidationResult isValid() throws SQLException {
// Lowest cost checks first
// Are message transactions even allowed at this point?
if (this.version != Transaction.getVersionByTimestamp(this.timestamp))
return ValidationResult.NOT_YET_RELEASED;
if (BlockChain.getHeight() < Block.MESSAGE_RELEASE_HEIGHT)
return ValidationResult.NOT_YET_RELEASED;
// Check data length
if (this.data.length < 1 || this.data.length > MAX_DATA_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
// Check recipient is a valid address
if (!Crypto.isValidAddress(this.recipient.getAddress()))
return ValidationResult.INVALID_ADDRESS;
if (this.version == 1) {
// Check amount is positive (V1)
if (this.amount.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)
return ValidationResult.NEGATIVE_AMOUNT;
}
// Check fee is positive
if (this.fee.compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_FEE;
// Check reference is correct
if (!Arrays.equals(this.sender.getLastReference(), this.reference))
return ValidationResult.INVALID_REFERENCE;
// Does asset exist? (This test not present in gen1)
if (this.assetId != Asset.QORA && !Asset.exists(this.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) {
// Check sender has enough funds for amount + fee in QORA
if (this.sender.getConfirmedBalance(Asset.QORA).compareTo(this.amount.add(this.fee)) == -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)
return ValidationResult.NO_BALANCE;
// Check sender has enough funds for fee in QORA
if (this.sender.getConfirmedBalance(Asset.QORA).compareTo(this.fee) == -1)
return ValidationResult.NO_BALANCE;
}
return ValidationResult.OK;
}
public void process() throws SQLException {
this.save();
// Update sender's balance due to amount
this.sender.setConfirmedBalance(this.assetId, this.sender.getConfirmedBalance(this.assetId).subtract(this.amount));
// Update sender's balance due to fee
this.sender.setConfirmedBalance(Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).subtract(this.fee));
// Update recipient's balance
this.recipient.setConfirmedBalance(this.assetId, this.recipient.getConfirmedBalance(this.assetId).add(this.amount));
// Update sender's reference
this.sender.setLastReference(this.signature);
// 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);
}
public void orphan() throws SQLException {
this.delete();
// Update sender's balance due to amount
this.sender.setConfirmedBalance(this.assetId, this.sender.getConfirmedBalance(this.assetId).add(this.amount));
// Update sender's balance due to fee
this.sender.setConfirmedBalance(Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).add(this.fee));
// Update recipient's balance
this.recipient.setConfirmedBalance(this.assetId, this.recipient.getConfirmedBalance(this.assetId).subtract(this.amount));
// Update sender's reference
this.sender.setLastReference(this.reference);
/*
* 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);
}
}