mirror of
https://github.com/Qortal/qortal.git
synced 2025-05-05 17:27:52 +00:00
PaymentTransaction now uses Account for recipient internally but maybe should extend that type change to constructor args. GenesisBlock added also with signature test. More javadocs.
370 lines
10 KiB
Java
370 lines
10 KiB
Java
package qora.transaction;
|
|
|
|
import java.math.BigDecimal;
|
|
import java.math.MathContext;
|
|
import java.sql.Connection;
|
|
import java.sql.PreparedStatement;
|
|
import java.sql.ResultSet;
|
|
import java.sql.SQLException;
|
|
import java.sql.Timestamp;
|
|
import java.time.Instant;
|
|
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 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;
|
|
|
|
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);
|
|
|
|
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
|
|
protected static final int CREATOR_LENGTH = 32;
|
|
|
|
// 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.
|
|
*
|
|
* @param connection
|
|
* @return height, or 0 if not in blockchain (i.e. unconfirmed)
|
|
* @throws SQLException
|
|
*/
|
|
public int getHeight(Connection connection) throws SQLException {
|
|
if (this.signature == null)
|
|
return 0;
|
|
|
|
BlockTransaction blockTx = BlockTransaction.fromTransactionSignature(connection, this.signature);
|
|
if (blockTx == null)
|
|
return 0;
|
|
|
|
return BlockChain.getBlockHeightFromSignature(connection, blockTx.getBlockSignature());
|
|
}
|
|
|
|
/**
|
|
* Get number of confirmations for this transaction.
|
|
*
|
|
* @param connection
|
|
* @return confirmation count, or 0 if not in blockchain (i.e. unconfirmed)
|
|
* @throws SQLException
|
|
*/
|
|
public int getConfirmations(Connection connection) throws SQLException {
|
|
int ourHeight = this.getHeight(connection);
|
|
if (ourHeight == 0)
|
|
return 0;
|
|
|
|
int blockChainHeight = BlockChain.getMaxHeight(connection);
|
|
return blockChainHeight - ourHeight + 1;
|
|
}
|
|
|
|
// Load/Save
|
|
|
|
// 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 connection
|
|
* @param type
|
|
* @param signature
|
|
* @throws NoDataFoundException
|
|
* if no matching row found
|
|
* @throws SQLException
|
|
*/
|
|
protected Transaction(Connection connection, TransactionType type, byte[] signature) throws SQLException {
|
|
ResultSet rs = DB.executeUsingBytes(connection, "SELECT reference, creator, creation, fee FROM Transactions WHERE signature = ?", signature);
|
|
if (rs == null)
|
|
throw new NoDataFoundException();
|
|
|
|
this.type = type;
|
|
this.reference = DB.getResultSetBytes(rs.getBinaryStream(1), REFERENCE_LENGTH);
|
|
this.creator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(2), CREATOR_LENGTH));
|
|
this.timestamp = rs.getTimestamp(3).getTime();
|
|
this.fee = rs.getBigDecimal(4).setScale(8);
|
|
this.signature = signature;
|
|
}
|
|
|
|
protected void save(Connection connection) throws SQLException {
|
|
String sql = DB.formatInsertWithPlaceholders("Transactions", "signature", "reference", "type", "creator", "creation", "fee", "milestone_block");
|
|
PreparedStatement preparedStatement = connection.prepareStatement(sql);
|
|
DB.bindInsertPlaceholders(preparedStatement, this.signature, this.reference, this.type.value, this.creator.getPublicKey(),
|
|
Timestamp.from(Instant.ofEpochSecond(this.timestamp)), this.fee, null);
|
|
preparedStatement.execute();
|
|
}
|
|
|
|
// Navigation
|
|
|
|
/**
|
|
* Load encapsulating Block from DB, if any
|
|
*
|
|
* @param connection
|
|
* @return Block, or null if transaction is not in a Block
|
|
* @throws SQLException
|
|
*/
|
|
public Block getBlock(Connection connection) throws SQLException {
|
|
if (this.signature == null)
|
|
return null;
|
|
|
|
BlockTransaction blockTx = BlockTransaction.fromTransactionSignature(connection, this.signature);
|
|
if (blockTx == null)
|
|
return null;
|
|
|
|
return Block.fromSignature(connection, blockTx.getBlockSignature());
|
|
}
|
|
|
|
/**
|
|
* Load parent Transaction from DB via this transaction's reference.
|
|
*
|
|
* @param connection
|
|
* @return Transaction, or null if no parent found (which should not happen)
|
|
* @throws SQLException
|
|
*/
|
|
public Transaction getParent(Connection connection) throws SQLException {
|
|
if (this.reference == null)
|
|
return null;
|
|
|
|
return TransactionFactory.fromSignature(connection, this.reference);
|
|
}
|
|
|
|
/**
|
|
* Load child Transaction from DB, if any.
|
|
*
|
|
* @param connection
|
|
* @return Transaction, or null if no child found
|
|
* @throws SQLException
|
|
*/
|
|
public Transaction getChild(Connection connection) throws SQLException {
|
|
if (this.signature == null)
|
|
return null;
|
|
|
|
return TransactionFactory.fromReference(connection, this.signature);
|
|
}
|
|
|
|
// Converters
|
|
|
|
public abstract JSONObject toJSON();
|
|
|
|
/**
|
|
* Produce JSON representation of common/base Transaction info.
|
|
* <p>
|
|
* To include info on number of confirmations, a Connection object is required. See {@link Transaction#getBaseJSON(Connection)}
|
|
*
|
|
* @return JSONObject
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
protected JSONObject getBaseJSON() {
|
|
JSONObject json = new JSONObject();
|
|
|
|
json.put("type", this.type.value);
|
|
json.put("fee", this.fee.toPlainString());
|
|
json.put("timestamp", this.timestamp);
|
|
json.put("reference", Base58.encode(this.reference));
|
|
json.put("signature", Base58.encode(this.signature));
|
|
|
|
return json;
|
|
}
|
|
|
|
/**
|
|
* Produce JSON representation of common/base Transaction info, including number of confirmations.
|
|
* <p>
|
|
* Requires SQL Connection object to determine number of confirmations.
|
|
*
|
|
* @param connection
|
|
* @return JSONObject
|
|
* @throws SQLException
|
|
* @see Transaction#getBaseJSON()
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
protected JSONObject getBaseJSON(Connection connection) throws SQLException {
|
|
JSONObject json = this.getBaseJSON();
|
|
|
|
json.put("confirmations", this.getConfirmations(connection));
|
|
|
|
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(PublicKeyAccount signer) {
|
|
if (this.signature == null)
|
|
return false;
|
|
|
|
return signer.verify(this.signature, this.toBytesLessSignature());
|
|
}
|
|
|
|
public abstract ValidationResult isValid(Connection connection);
|
|
|
|
public abstract void process();
|
|
|
|
public abstract void orphan();
|
|
|
|
}
|