mirror of
https://github.com/Qortal/qortal.git
synced 2025-03-30 09:05:52 +00:00
No need for DB.executeUsingBytes as it was only a specific use-case for DB.checkedExecute. Callers now refactored to use DB.checkedExecute instead. Minor tidying up of BlockTransactions in light of above. In the HSQLDB database, asset keys/IDs are now "asset_id" (previously: "asset"). Added initial Asset/Order/Trade classes. Added CreateOrderTransaction class. Renamed some asset-related fields back to old gen1 names, e.g. haveAmount -> amount, wantAmount -> price. Added Accounts and AccountBalances to database. Added get/set confirmed balance support to Account. Added get/set last reference support to Account. Added Block.toJSON() - untested at this time. Fleshed out some Transaction sub-classes' process() and orphan() methods. Fleshed out PaymentTransaction.isValid(). Added Transaction.delete() - untested.
380 lines
10 KiB
Java
380 lines
10 KiB
Java
package qora.transaction;
|
|
|
|
import java.math.BigDecimal;
|
|
import java.math.MathContext;
|
|
import java.nio.ByteBuffer;
|
|
import java.sql.Connection;
|
|
import java.sql.ResultSet;
|
|
import java.sql.SQLException;
|
|
import java.sql.Timestamp;
|
|
import java.util.Arrays;
|
|
import java.util.Map;
|
|
import static java.util.Arrays.stream;
|
|
import static java.util.stream.Collectors.toMap;
|
|
|
|
import org.json.simple.JSONObject;
|
|
|
|
import database.DB;
|
|
import database.NoDataFoundException;
|
|
import database.SaveHelper;
|
|
import qora.account.PrivateKeyAccount;
|
|
import qora.account.PublicKeyAccount;
|
|
import qora.block.Block;
|
|
import qora.block.BlockChain;
|
|
import qora.block.BlockTransaction;
|
|
import settings.Settings;
|
|
|
|
import utils.Base58;
|
|
import utils.ParseException;
|
|
|
|
public abstract class Transaction {
|
|
|
|
// Transaction types
|
|
public enum TransactionType {
|
|
GENESIS(1), PAYMENT(2), REGISTER_NAME(3), UPDATE_NAME(4), SELL_NAME(5), CANCEL_SELL_NAME(6), BUY_NAME(7), CREATE_POLL(8), VOTE_ON_POLL(9), ARBITRARY(
|
|
10), ISSUE_ASSET(11), TRANSFER_ASSET(12), CREATE_ASSET_ORDER(13), CANCEL_ASSET_ORDER(14), MULTIPAYMENT(15), DEPLOY_AT(16), MESSAGE(17);
|
|
|
|
public final int value;
|
|
|
|
private final static Map<Integer, TransactionType> map = stream(TransactionType.values()).collect(toMap(type -> type.value, type -> type));
|
|
|
|
TransactionType(int value) {
|
|
this.value = value;
|
|
}
|
|
|
|
public static TransactionType valueOf(int value) {
|
|
return map.get(value);
|
|
}
|
|
}
|
|
|
|
// Validation results
|
|
public enum ValidationResult {
|
|
OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6);
|
|
|
|
public final int value;
|
|
|
|
private final static Map<Integer, ValidationResult> map = stream(ValidationResult.values()).collect(toMap(result -> result.value, result -> result));
|
|
|
|
ValidationResult(int value) {
|
|
this.value = value;
|
|
}
|
|
|
|
public static ValidationResult valueOf(int value) {
|
|
return map.get(value);
|
|
}
|
|
}
|
|
|
|
// Minimum fee
|
|
public static final BigDecimal MINIMUM_FEE = BigDecimal.ONE;
|
|
|
|
// Cached info to make transaction processing faster
|
|
protected static final BigDecimal maxBytePerFee = BigDecimal.valueOf(Settings.getInstance().getMaxBytePerFee());
|
|
protected static final BigDecimal minFeePerByte = BigDecimal.ONE.divide(maxBytePerFee, MathContext.DECIMAL32);
|
|
|
|
// Database properties shared with all transaction types
|
|
protected TransactionType type;
|
|
protected PublicKeyAccount creator;
|
|
protected long timestamp;
|
|
protected byte[] reference;
|
|
protected BigDecimal fee;
|
|
protected byte[] signature;
|
|
|
|
// Derived/cached properties
|
|
|
|
// Property lengths for serialisation
|
|
protected static final int TYPE_LENGTH = 4;
|
|
protected static final int TIMESTAMP_LENGTH = 8;
|
|
protected static final int REFERENCE_LENGTH = 64;
|
|
protected static final int FEE_LENGTH = 8;
|
|
public static final int SIGNATURE_LENGTH = 64;
|
|
protected static final int BASE_TYPELESS_LENGTH = TIMESTAMP_LENGTH + REFERENCE_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH;
|
|
|
|
// Other length constants
|
|
public static final int CREATOR_LENGTH = 32;
|
|
public static final int RECIPIENT_LENGTH = 25;
|
|
|
|
// Constructors
|
|
|
|
protected Transaction(TransactionType type, BigDecimal fee, PublicKeyAccount creator, long timestamp, byte[] reference, byte[] signature) {
|
|
this.fee = fee;
|
|
this.type = type;
|
|
this.creator = creator;
|
|
this.timestamp = timestamp;
|
|
this.reference = reference;
|
|
this.signature = signature;
|
|
}
|
|
|
|
protected Transaction(TransactionType type, BigDecimal fee, PublicKeyAccount creator, long timestamp, byte[] reference) {
|
|
this(type, fee, creator, timestamp, reference, null);
|
|
}
|
|
|
|
// Getters/setters
|
|
|
|
public TransactionType getType() {
|
|
return this.type;
|
|
}
|
|
|
|
public PublicKeyAccount getCreator() {
|
|
return this.creator;
|
|
}
|
|
|
|
public long getTimestamp() {
|
|
return this.timestamp;
|
|
}
|
|
|
|
public byte[] getReference() {
|
|
return this.reference;
|
|
}
|
|
|
|
public BigDecimal getFee() {
|
|
return this.fee;
|
|
}
|
|
|
|
public byte[] getSignature() {
|
|
return this.signature;
|
|
}
|
|
|
|
// More information
|
|
|
|
public long getDeadline() {
|
|
// 24 hour deadline to include transaction in a block
|
|
return this.timestamp + (24 * 60 * 60 * 1000);
|
|
}
|
|
|
|
public abstract int getDataLength();
|
|
|
|
public boolean hasMinimumFee() {
|
|
return this.fee.compareTo(MINIMUM_FEE) >= 0;
|
|
}
|
|
|
|
public BigDecimal feePerByte() {
|
|
return this.fee.divide(new BigDecimal(this.getDataLength()), MathContext.DECIMAL32);
|
|
}
|
|
|
|
public boolean hasMinimumFeePerByte() {
|
|
return this.feePerByte().compareTo(minFeePerByte) >= 0;
|
|
}
|
|
|
|
public BigDecimal calcRecommendedFee() {
|
|
BigDecimal recommendedFee = BigDecimal.valueOf(this.getDataLength()).divide(maxBytePerFee, MathContext.DECIMAL32).setScale(8);
|
|
|
|
// security margin
|
|
recommendedFee = recommendedFee.add(new BigDecimal("0.0000001"));
|
|
|
|
if (recommendedFee.compareTo(MINIMUM_FEE) <= 0) {
|
|
recommendedFee = MINIMUM_FEE;
|
|
} else {
|
|
recommendedFee = recommendedFee.setScale(0, BigDecimal.ROUND_UP);
|
|
}
|
|
|
|
return recommendedFee.setScale(8);
|
|
}
|
|
|
|
/**
|
|
* Get block height for this transaction in the blockchain.
|
|
*
|
|
* @return height, or 0 if not in blockchain (i.e. unconfirmed)
|
|
* @throws SQLException
|
|
*/
|
|
public int getHeight() throws SQLException {
|
|
if (this.signature == null)
|
|
return 0;
|
|
|
|
BlockTransaction blockTx = BlockTransaction.fromTransactionSignature(this.signature);
|
|
if (blockTx == null)
|
|
return 0;
|
|
|
|
return BlockChain.getBlockHeightFromSignature(blockTx.getBlockSignature());
|
|
}
|
|
|
|
/**
|
|
* Get number of confirmations for this transaction.
|
|
*
|
|
* @return confirmation count, or 0 if not in blockchain (i.e. unconfirmed)
|
|
* @throws SQLException
|
|
*/
|
|
public int getConfirmations() throws SQLException {
|
|
int ourHeight = this.getHeight();
|
|
if (ourHeight == 0)
|
|
return 0;
|
|
|
|
int blockChainHeight = BlockChain.getHeight();
|
|
return blockChainHeight - ourHeight + 1;
|
|
}
|
|
|
|
// Load/Save/Delete
|
|
|
|
// Typically called by sub-class' load-from-DB constructors
|
|
|
|
/**
|
|
* Load base Transaction from DB using signature.
|
|
* <p>
|
|
* Note that the transaction type is <b>not</b> checked against the DB's value.
|
|
*
|
|
* @param type
|
|
* @param signature
|
|
* @throws NoDataFoundException
|
|
* if no matching row found
|
|
* @throws SQLException
|
|
*/
|
|
protected Transaction(TransactionType type, byte[] signature) throws SQLException {
|
|
ResultSet rs = DB.checkedExecute("SELECT reference, creator, creation, fee FROM Transactions WHERE signature = ? AND type = ?", signature, type.value);
|
|
if (rs == null)
|
|
throw new NoDataFoundException();
|
|
|
|
this.type = type;
|
|
this.reference = DB.getResultSetBytes(rs.getBinaryStream(1), REFERENCE_LENGTH);
|
|
// Note: can't use CREATOR_LENGTH in case we encounter Genesis Account's short, 8-byte public key
|
|
this.creator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(2)));
|
|
this.timestamp = rs.getTimestamp(3).getTime();
|
|
this.fee = rs.getBigDecimal(4).setScale(8);
|
|
this.signature = signature;
|
|
}
|
|
|
|
protected void save(Connection connection) throws SQLException {
|
|
SaveHelper saveHelper = new SaveHelper(connection, "Transactions");
|
|
saveHelper.bind("signature", this.signature).bind("reference", this.reference).bind("type", this.type.value)
|
|
.bind("creator", this.creator.getPublicKey()).bind("creation", new Timestamp(this.timestamp)).bind("fee", this.fee)
|
|
.bind("milestone_block", null);
|
|
saveHelper.execute();
|
|
}
|
|
|
|
protected void delete(Connection connection) throws SQLException {
|
|
DB.checkedExecute("DELETE FROM Transactions WHERE signature = ?", this.signature);
|
|
}
|
|
|
|
// Navigation
|
|
|
|
/**
|
|
* Load encapsulating Block from DB, if any
|
|
*
|
|
* @return Block, or null if transaction is not in a Block
|
|
* @throws SQLException
|
|
*/
|
|
public Block getBlock() throws SQLException {
|
|
if (this.signature == null)
|
|
return null;
|
|
|
|
BlockTransaction blockTx = BlockTransaction.fromTransactionSignature(this.signature);
|
|
if (blockTx == null)
|
|
return null;
|
|
|
|
return Block.fromSignature(blockTx.getBlockSignature());
|
|
}
|
|
|
|
/**
|
|
* Load parent Transaction from DB via this transaction's reference.
|
|
*
|
|
* @return Transaction, or null if no parent found (which should not happen)
|
|
* @throws SQLException
|
|
*/
|
|
public Transaction getParent() throws SQLException {
|
|
if (this.reference == null)
|
|
return null;
|
|
|
|
return TransactionFactory.fromSignature(this.reference);
|
|
}
|
|
|
|
/**
|
|
* Load child Transaction from DB, if any.
|
|
*
|
|
* @return Transaction, or null if no child found
|
|
* @throws SQLException
|
|
*/
|
|
public Transaction getChild() throws SQLException {
|
|
if (this.signature == null)
|
|
return null;
|
|
|
|
return TransactionFactory.fromReference(this.signature);
|
|
}
|
|
|
|
// Converters
|
|
|
|
public static Transaction parse(byte[] data) throws ParseException {
|
|
if (data == null)
|
|
return null;
|
|
|
|
if (data.length < TYPE_LENGTH)
|
|
throw new ParseException("Byte data too short to determine transaction type");
|
|
|
|
ByteBuffer byteBuffer = ByteBuffer.wrap(data);
|
|
|
|
TransactionType type = TransactionType.valueOf(byteBuffer.getInt());
|
|
if (type == null)
|
|
return null;
|
|
|
|
switch (type) {
|
|
case GENESIS:
|
|
return GenesisTransaction.parse(byteBuffer);
|
|
|
|
case PAYMENT:
|
|
return PaymentTransaction.parse(byteBuffer);
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public abstract JSONObject toJSON() throws SQLException;
|
|
|
|
/**
|
|
* Produce JSON representation of common/base Transaction info.
|
|
*
|
|
* @return JSONObject
|
|
* @throws SQLException
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
protected JSONObject getBaseJSON() throws SQLException {
|
|
JSONObject json = new JSONObject();
|
|
|
|
json.put("type", this.type.value);
|
|
json.put("fee", this.fee.toPlainString());
|
|
json.put("timestamp", this.timestamp);
|
|
json.put("signature", Base58.encode(this.signature));
|
|
|
|
if (this.reference != null)
|
|
json.put("reference", Base58.encode(this.reference));
|
|
|
|
json.put("confirmations", this.getConfirmations());
|
|
|
|
return json;
|
|
}
|
|
|
|
/**
|
|
* Serialize transaction as byte[], including signature.
|
|
*
|
|
* @return byte[]
|
|
*/
|
|
public abstract byte[] toBytes();
|
|
|
|
/**
|
|
* Serialize transaction as byte[], stripping off trailing signature.
|
|
*
|
|
* @return byte[]
|
|
*/
|
|
private byte[] toBytesLessSignature() {
|
|
byte[] bytes = this.toBytes();
|
|
return Arrays.copyOf(bytes, bytes.length - SIGNATURE_LENGTH);
|
|
}
|
|
|
|
// Processing
|
|
|
|
public byte[] calcSignature(PrivateKeyAccount signer) {
|
|
return signer.sign(this.toBytesLessSignature());
|
|
}
|
|
|
|
public boolean isSignatureValid() {
|
|
if (this.signature == null)
|
|
return false;
|
|
|
|
return this.creator.verify(this.signature, this.toBytesLessSignature());
|
|
}
|
|
|
|
public abstract ValidationResult isValid(Connection connection) throws SQLException;
|
|
|
|
public abstract void process(Connection connection) throws SQLException;
|
|
|
|
public abstract void orphan(Connection connection) throws SQLException;
|
|
|
|
}
|