diff --git a/src/data/transaction/PaymentTransactionData.java b/src/data/transaction/PaymentTransactionData.java new file mode 100644 index 00000000..98afb3ab --- /dev/null +++ b/src/data/transaction/PaymentTransactionData.java @@ -0,0 +1,43 @@ +package data.transaction; + +import java.math.BigDecimal; + +import qora.transaction.Transaction.TransactionType; + +public class PaymentTransactionData extends TransactionData { + + // Properties + private byte[] senderPublicKey; + private String recipient; + private BigDecimal amount; + + // Constructors + + public PaymentTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference, + byte[] signature) { + super(TransactionType.PAYMENT, fee, senderPublicKey, timestamp, reference, signature); + + this.senderPublicKey = senderPublicKey; + this.recipient = recipient; + this.amount = amount; + } + + public PaymentTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference) { + this(senderPublicKey, recipient, amount, fee, timestamp, reference, null); + } + + // Getters/Setters + + public byte[] getSenderPublicKey() { + return this.senderPublicKey; + } + + public String getRecipient() { + return this.recipient; + } + + public BigDecimal getAmount() { + return this.amount; + } + +} diff --git a/src/qora/transaction/PaymentTransaction.java b/src/qora/transaction/PaymentTransaction.java index 2a51afad..1dbd841f 100644 --- a/src/qora/transaction/PaymentTransaction.java +++ b/src/qora/transaction/PaymentTransaction.java @@ -1,239 +1,103 @@ package qora.transaction; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.math.BigDecimal; -import java.nio.ByteBuffer; -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.PaymentTransactionData; +import data.transaction.TransactionData; import qora.account.Account; import qora.account.PublicKeyAccount; import qora.assets.Asset; 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 PaymentTransaction extends TransactionHandler { - - // Properties - private PublicKeyAccount sender; - private Account recipient; - private BigDecimal amount; - - // Property lengths - private static final int SENDER_LENGTH = 32; - private static final int AMOUNT_LENGTH = 8; - private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH; +public class PaymentTransaction extends Transaction { // Constructors - public PaymentTransaction(PublicKeyAccount sender, String recipient, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference, - byte[] signature) { - super(TransactionType.PAYMENT, fee, sender, timestamp, reference, signature); - - this.sender = sender; - this.recipient = new Account(recipient); - this.amount = amount; - } - - public PaymentTransaction(PublicKeyAccount sender, String recipient, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference) { - this(sender, recipient, amount, fee, timestamp, reference, null); - } - - // Getters/Setters - - public PublicKeyAccount getSender() { - return this.sender; - } - - public Account getRecipient() { - return this.recipient; - } - - public BigDecimal getAmount() { - return this.amount; - } - - // More information - - public int getDataLength() { - return TYPE_LENGTH + TYPELESS_LENGTH; - } - - // Load/Save - - /** - * Construct PaymentTransaction from DB using signature. - * - * @param signature - * @throws NoDataFoundException - * if no matching row found - * @throws SQLException - */ - protected PaymentTransaction(byte[] signature) throws SQLException { - super(TransactionType.PAYMENT, signature); - - ResultSet rs = DB.checkedExecute("SELECT sender, recipient, amount FROM PaymentTransactions WHERE signature = ?", signature); - if (rs == null) - throw new NoDataFoundException(); - - this.sender = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(1), CREATOR_LENGTH)); - this.recipient = new Account(rs.getString(2)); - this.amount = rs.getBigDecimal(3).setScale(8); - } - - /** - * Load PaymentTransaction from DB using signature. - * - * @param signature - * @return PaymentTransaction, or null if not found - * @throws SQLException - */ - public static PaymentTransaction fromSignature(byte[] signature) throws SQLException { - try { - return new PaymentTransaction(signature); - } catch (NoDataFoundException e) { - return null; - } - } - - @Override - public void save() throws SQLException { - super.save(); - - HSQLDBSaver saveHelper = new HSQLDBSaver("PaymentTransactions"); - saveHelper.bind("signature", this.signature).bind("sender", this.sender.getPublicKey()).bind("recipient", this.recipient.getAddress()).bind("amount", - this.amount); - saveHelper.execute(); - } - - // Converters - - protected static TransactionHandler parse(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_LENGTH) - throw new TransformationException("Byte data too short for PaymentTransaction"); - - long timestamp = byteBuffer.getLong(); - - byte[] reference = new byte[REFERENCE_LENGTH]; - byteBuffer.get(reference); - - PublicKeyAccount sender = Serialization.deserializePublicKey(byteBuffer); - String recipient = Serialization.deserializeRecipient(byteBuffer); - BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); - BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); - - byte[] signature = new byte[SIGNATURE_LENGTH]; - byteBuffer.get(signature); - - return new PaymentTransaction(sender, recipient, amount, fee, timestamp, reference, signature); - } - - @SuppressWarnings("unchecked") - @Override - public JSONObject toJSON() throws SQLException { - JSONObject json = getBaseJSON(); - - 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()); - - 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())); - bytes.write(Serialization.serializeBigDecimal(this.amount)); - bytes.write(Serialization.serializeBigDecimal(this.fee)); - bytes.write(this.signature); - return bytes.toByteArray(); - } catch (IOException e) { - throw new RuntimeException(e); - } + public PaymentTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); } // Processing - public ValidationResult isValid() throws SQLException { + public ValidationResult isValid() throws DataException { // Lowest cost checks first + PaymentTransactionData paymentTransactionData = (PaymentTransactionData) this.transactionData; + // Check recipient is a valid address - if (!Crypto.isValidAddress(this.recipient.getAddress())) + if (!Crypto.isValidAddress(paymentTransactionData.getRecipient())) return ValidationResult.INVALID_ADDRESS; // Check amount is positive - if (this.amount.compareTo(BigDecimal.ZERO) <= 0) + if (paymentTransactionData.getAmount().compareTo(BigDecimal.ZERO) <= 0) return ValidationResult.NEGATIVE_AMOUNT; // Check fee is positive - if (this.fee.compareTo(BigDecimal.ZERO) <= 0) + if (paymentTransactionData.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(repository, paymentTransactionData.getSenderPublicKey()); + if (!Arrays.equals(sender.getLastReference(), paymentTransactionData.getReference())) return ValidationResult.INVALID_REFERENCE; // Check sender has enough funds - if (this.sender.getConfirmedBalance(Asset.QORA).compareTo(this.amount.add(this.fee)) == -1) + if (sender.getConfirmedBalance(Asset.QORA).compareTo(paymentTransactionData.getAmount().add(paymentTransactionData.getFee())) == -1) return ValidationResult.NO_BALANCE; return ValidationResult.OK; } - public void process() throws SQLException { - this.save(); + public void process() throws DataException { + PaymentTransactionData paymentTransactionData = (PaymentTransactionData) this.transactionData; + + // Save this transaction itself + this.repository.getTransactionRepository().save(this.transactionData); // Update sender's balance - this.sender.setConfirmedBalance(Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).subtract(this.amount).subtract(this.fee)); + Account sender = new PublicKeyAccount(repository, paymentTransactionData.getSenderPublicKey()); + sender.setConfirmedBalance(Asset.QORA, + sender.getConfirmedBalance(Asset.QORA).subtract(paymentTransactionData.getAmount()).subtract(paymentTransactionData.getFee())); // Update recipient's balance - this.recipient.setConfirmedBalance(Asset.QORA, this.recipient.getConfirmedBalance(Asset.QORA).add(this.amount)); + Account recipient = new Account(repository, paymentTransactionData.getRecipient()); + recipient.setConfirmedBalance(Asset.QORA, recipient.getConfirmedBalance(Asset.QORA).add(paymentTransactionData.getAmount())); // Update sender's reference - this.sender.setLastReference(this.signature); + sender.setLastReference(paymentTransactionData.getSignature()); // If recipient has no reference yet, then this is their starting reference - if (this.recipient.getLastReference() == null) - this.recipient.setLastReference(this.signature); + if (recipient.getLastReference() == null) + recipient.setLastReference(paymentTransactionData.getSignature()); } - public void orphan() throws SQLException { - this.delete(); + public void orphan() throws DataException { + PaymentTransactionData paymentTransactionData = (PaymentTransactionData) this.transactionData; + + // Delete this transaction + this.repository.getTransactionRepository().delete(this.transactionData); // Update sender's balance - this.sender.setConfirmedBalance(Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).add(this.amount).add(this.fee)); + Account sender = new PublicKeyAccount(repository, paymentTransactionData.getSenderPublicKey()); + sender.setConfirmedBalance(Asset.QORA, + sender.getConfirmedBalance(Asset.QORA).add(paymentTransactionData.getAmount()).add(paymentTransactionData.getFee())); // Update recipient's balance - this.recipient.setConfirmedBalance(Asset.QORA, this.recipient.getConfirmedBalance(Asset.QORA).subtract(this.amount)); + Account recipient = new Account(repository, paymentTransactionData.getRecipient()); + recipient.setConfirmedBalance(Asset.QORA, recipient.getConfirmedBalance(Asset.QORA).subtract(paymentTransactionData.getAmount())); // Update sender's reference - this.sender.setLastReference(this.reference); + sender.setLastReference(paymentTransactionData.getReference()); /* * 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 (Arrays.equals(this.recipient.getLastReference(), this.signature)) - this.recipient.setLastReference(null); + if (Arrays.equals(recipient.getLastReference(), paymentTransactionData.getSignature())) + recipient.setLastReference(null); } } diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index 7f9255e9..dfc95047 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -80,9 +80,15 @@ public abstract class Transaction { case GENESIS: return new GenesisTransaction(repository, transactionData); + case PAYMENT: + return new PaymentTransaction(repository, transactionData); + case ISSUE_ASSET: return new IssueAssetTransaction(repository, transactionData); + case CREATE_ASSET_ORDER: + return new CreateOrderTransaction(repository, transactionData); + default: return null; } @@ -224,7 +230,7 @@ public abstract class Transaction { return Arrays.copyOf(bytes, bytes.length - Transformer.SIGNATURE_LENGTH); } catch (TransformationException e) { // XXX this isn't good - return null; + throw new RuntimeException("Unable to transform transaction to signature-less byte array", e); } } diff --git a/src/repository/hsqldb/transaction/HSQLDBPaymentTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBPaymentTransactionRepository.java new file mode 100644 index 00000000..bf1bbfcb --- /dev/null +++ b/src/repository/hsqldb/transaction/HSQLDBPaymentTransactionRepository.java @@ -0,0 +1,53 @@ +package repository.hsqldb.transaction; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import data.transaction.PaymentTransactionData; +import data.transaction.TransactionData; +import repository.DataException; +import repository.hsqldb.HSQLDBRepository; +import repository.hsqldb.HSQLDBSaver; + +public class HSQLDBPaymentTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBPaymentTransactionRepository(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 sender, recipient, amount FROM PaymentTransactions WHERE signature = ?", signature); + if (rs == null) + return null; + + byte[] senderPublicKey = this.repository.getResultSetBytes(rs.getBinaryStream(1)); + String recipient = rs.getString(2); + BigDecimal amount = rs.getBigDecimal(3); + + return new PaymentTransactionData(senderPublicKey, recipient, amount, fee, timestamp, reference, signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch payment transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + super.save(transactionData); + + PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("PaymentTransactions"); + + saveHelper.bind("signature", paymentTransactionData.getSignature()).bind("sender", paymentTransactionData.getSenderPublicKey()) + .bind("recipient", paymentTransactionData.getRecipient()).bind("amount", paymentTransactionData.getAmount()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save payment transaction into repository", e); + } + } + +} diff --git a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index dbf25cdc..12fff172 100644 --- a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -17,12 +17,14 @@ public class HSQLDBTransactionRepository implements TransactionRepository { protected HSQLDBRepository repository; private HSQLDBGenesisTransactionRepository genesisTransactionRepository; + private HSQLDBPaymentTransactionRepository paymentTransactionRepository; private HSQLDBIssueAssetTransactionRepository issueAssetTransactionRepository; private HSQLDBCreateOrderTransactionRepository createOrderTransactionRepository; public HSQLDBTransactionRepository(HSQLDBRepository repository) { this.repository = repository; this.genesisTransactionRepository = new HSQLDBGenesisTransactionRepository(repository); + this.paymentTransactionRepository = new HSQLDBPaymentTransactionRepository(repository); this.issueAssetTransactionRepository = new HSQLDBIssueAssetTransactionRepository(repository); this.createOrderTransactionRepository = new HSQLDBCreateOrderTransactionRepository(repository); } @@ -69,6 +71,9 @@ public class HSQLDBTransactionRepository implements TransactionRepository { case GENESIS: return this.genesisTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case PAYMENT: + return this.paymentTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case ISSUE_ASSET: return this.issueAssetTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); diff --git a/src/transform/transaction/CreateOrderTransactionTransformer.java b/src/transform/transaction/CreateOrderTransactionTransformer.java index 29e84984..f4aa69b4 100644 --- a/src/transform/transaction/CreateOrderTransactionTransformer.java +++ b/src/transform/transaction/CreateOrderTransactionTransformer.java @@ -72,6 +72,7 @@ public class CreateOrderTransactionTransformer extends TransactionTransformer { Serialization.serializeBigDecimal(createOrderTransactionData.getAmount(), AMOUNT_LENGTH); Serialization.serializeBigDecimal(createOrderTransactionData.getPrice(), AMOUNT_LENGTH); + Serialization.serializeBigDecimal(createOrderTransactionData.getFee()); bytes.write(createOrderTransactionData.getSignature()); return bytes.toByteArray(); diff --git a/src/transform/transaction/IssueAssetTransactionTransformer.java b/src/transform/transaction/IssueAssetTransactionTransformer.java index dba77495..0ce285c9 100644 --- a/src/transform/transaction/IssueAssetTransactionTransformer.java +++ b/src/transform/transaction/IssueAssetTransactionTransformer.java @@ -90,6 +90,7 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { bytes.write(Longs.toByteArray(issueAssetTransactionData.getQuantity())); bytes.write((byte) (issueAssetTransactionData.getIsDivisible() ? 1 : 0)); + Serialization.serializeBigDecimal(issueAssetTransactionData.getFee()); bytes.write(issueAssetTransactionData.getSignature()); return bytes.toByteArray(); diff --git a/src/transform/transaction/PaymentTransactionTransformer.java b/src/transform/transaction/PaymentTransactionTransformer.java new file mode 100644 index 00000000..60c87a0d --- /dev/null +++ b/src/transform/transaction/PaymentTransactionTransformer.java @@ -0,0 +1,99 @@ +package transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; + +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 data.transaction.PaymentTransactionData; +import transform.TransformationException; +import utils.Base58; +import utils.Serialization; + +public class PaymentTransactionTransformer 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 TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + if (byteBuffer.remaining() < TYPELESS_LENGTH) + throw new TransformationException("Byte data too short for PaymentTransaction"); + + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer); + String recipient = Serialization.deserializeRecipient(byteBuffer); + BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new PaymentTransactionData(senderPublicKey, recipient, amount, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + return TYPE_LENGTH + TYPELESS_LENGTH; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(paymentTransactionData.getType().value)); + bytes.write(Longs.toByteArray(paymentTransactionData.getTimestamp())); + bytes.write(paymentTransactionData.getReference()); + + bytes.write(paymentTransactionData.getSenderPublicKey()); + bytes.write(Base58.decode(paymentTransactionData.getRecipient())); + + Serialization.serializeBigDecimal(paymentTransactionData.getAmount()); + + Serialization.serializeBigDecimal(paymentTransactionData.getFee()); + bytes.write(paymentTransactionData.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 { + PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData; + + byte[] senderPublicKey = paymentTransactionData.getSenderPublicKey(); + + json.put("sender", PublicKeyAccount.getAddress(senderPublicKey)); + json.put("senderPublicKey", HashCode.fromBytes(senderPublicKey).toString()); + json.put("recipient", paymentTransactionData.getRecipient()); + json.put("amount", paymentTransactionData.getAmount().toPlainString()); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/transform/transaction/TransactionTransformer.java b/src/transform/transaction/TransactionTransformer.java index 29f74dff..e8d6f5a7 100644 --- a/src/transform/transaction/TransactionTransformer.java +++ b/src/transform/transaction/TransactionTransformer.java @@ -33,6 +33,9 @@ public class TransactionTransformer extends Transformer { case GENESIS: return GenesisTransactionTransformer.fromByteBuffer(byteBuffer); + case PAYMENT: + return PaymentTransactionTransformer.fromByteBuffer(byteBuffer); + case ISSUE_ASSET: return IssueAssetTransactionTransformer.fromByteBuffer(byteBuffer); @@ -49,6 +52,9 @@ public class TransactionTransformer extends Transformer { case GENESIS: return GenesisTransactionTransformer.getDataLength(transactionData); + case PAYMENT: + return PaymentTransactionTransformer.getDataLength(transactionData); + case ISSUE_ASSET: return IssueAssetTransactionTransformer.getDataLength(transactionData); @@ -65,6 +71,9 @@ public class TransactionTransformer extends Transformer { case GENESIS: return GenesisTransactionTransformer.toBytes(transactionData); + case PAYMENT: + return PaymentTransactionTransformer.toBytes(transactionData); + case ISSUE_ASSET: return IssueAssetTransactionTransformer.toBytes(transactionData); @@ -81,6 +90,9 @@ public class TransactionTransformer extends Transformer { case GENESIS: return GenesisTransactionTransformer.toJSON(transaction); + case PAYMENT: + return PaymentTransactionTransformer.toJSON(transaction); + case ISSUE_ASSET: return IssueAssetTransactionTransformer.toJSON(transaction);