diff --git a/src/data/PaymentData.java b/src/data/PaymentData.java new file mode 100644 index 00000000..c42e24c2 --- /dev/null +++ b/src/data/PaymentData.java @@ -0,0 +1,34 @@ +package data; + +import java.math.BigDecimal; + +public class PaymentData { + + // Properties + protected String recipient; + protected long assetId; + protected BigDecimal amount; + + // Constructors + + public PaymentData(String recipient, long assetId, BigDecimal amount) { + this.recipient = recipient; + this.assetId = assetId; + this.amount = amount; + } + + // Getters/setters + + public String getRecipient() { + return this.recipient; + } + + public long getAssetId() { + return this.assetId; + } + + public BigDecimal getAmount() { + return this.amount; + } + +} diff --git a/src/data/assets/OrderData.java b/src/data/assets/OrderData.java index 018638b7..8f1d5fe9 100644 --- a/src/data/assets/OrderData.java +++ b/src/data/assets/OrderData.java @@ -12,9 +12,10 @@ public class OrderData implements Comparable { private BigDecimal fulfilled; private BigDecimal price; private long timestamp; + private boolean isClosed; public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal price, - long timestamp) { + long timestamp, boolean isClosed) { this.orderId = orderId; this.creatorPublicKey = creatorPublicKey; this.haveAssetId = haveAssetId; @@ -23,10 +24,11 @@ public class OrderData implements Comparable { this.fulfilled = fulfilled; this.price = price; this.timestamp = timestamp; + this.isClosed = isClosed; } public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal price, long timestamp) { - this(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, BigDecimal.ZERO.setScale(8), price, timestamp); + this(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, BigDecimal.ZERO.setScale(8), price, timestamp, false); } public byte[] getOrderId() { @@ -65,6 +67,14 @@ public class OrderData implements Comparable { return this.timestamp; } + public boolean getIsClosed() { + return this.isClosed; + } + + public void setIsClosed(boolean isClosed) { + this.isClosed = isClosed; + } + @Override public int compareTo(OrderData orderData) { // Compare using prices diff --git a/src/data/transaction/CancelOrderTransactionData.java b/src/data/transaction/CancelOrderTransactionData.java new file mode 100644 index 00000000..1c025940 --- /dev/null +++ b/src/data/transaction/CancelOrderTransactionData.java @@ -0,0 +1,32 @@ +package data.transaction; + +import java.math.BigDecimal; + +import qora.transaction.Transaction; + +public class CancelOrderTransactionData extends TransactionData { + + // Properties + private byte[] creatorPublicKey; + private byte[] orderId; + + // Constructors + + public CancelOrderTransactionData(byte[] creatorPublicKey, byte[] orderId, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + super(Transaction.TransactionType.CANCEL_ASSET_ORDER, fee, creatorPublicKey, timestamp, reference, signature); + + this.creatorPublicKey = creatorPublicKey; + this.orderId = orderId; + } + + // Getters/Setters + + public byte[] getCreatorPublicKey() { + return this.creatorPublicKey; + } + + public byte[] getOrderId() { + return this.orderId; + } + +} diff --git a/src/data/transaction/MultiPaymentTransactionData.java b/src/data/transaction/MultiPaymentTransactionData.java new file mode 100644 index 00000000..4d3c9790 --- /dev/null +++ b/src/data/transaction/MultiPaymentTransactionData.java @@ -0,0 +1,39 @@ +package data.transaction; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import data.PaymentData; +import qora.transaction.Transaction; + +public class MultiPaymentTransactionData extends TransactionData { + + // Properties + private byte[] senderPublicKey; + private List payments; + + // Constructors + + public MultiPaymentTransactionData(byte[] senderPublicKey, List payments, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + super(Transaction.TransactionType.MULTIPAYMENT, fee, senderPublicKey, timestamp, reference, signature); + + this.senderPublicKey = senderPublicKey; + this.payments = payments; + } + + public MultiPaymentTransactionData(byte[] senderPublicKey, BigDecimal fee, long timestamp, byte[] reference) { + this(senderPublicKey, new ArrayList(), fee, timestamp, reference, null); + } + + // Getters/setters + + public byte[] getSenderPublicKey() { + return this.senderPublicKey; + } + + public List getPayments() { + return this.payments; + } + +} diff --git a/src/qora/assets/Order.java b/src/qora/assets/Order.java index c9fb21eb..d64fecef 100644 --- a/src/qora/assets/Order.java +++ b/src/qora/assets/Order.java @@ -54,11 +54,26 @@ public class Order { // Processing public void process() throws DataException { + this.repository.getAssetRepository().save(this.orderData); + // TODO } public void orphan() throws DataException { // TODO + + this.repository.getAssetRepository().delete(this.orderData.getOrderId()); + } + + // This is CancelOrderTransactions so that an Order can no longer trade + public void cancel() throws DataException { + this.orderData.setIsClosed(true); + this.repository.getAssetRepository().save(this.orderData); + } + + // Opposite of cancel() above for use during orphaning + public void reopen() throws DataException { + // TODO } } diff --git a/src/qora/payment/Payment.java b/src/qora/payment/Payment.java new file mode 100644 index 00000000..86782a9a --- /dev/null +++ b/src/qora/payment/Payment.java @@ -0,0 +1,156 @@ +package qora.payment; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import data.PaymentData; +import data.assets.AssetData; +import qora.account.Account; +import qora.account.PublicKeyAccount; +import qora.assets.Asset; +import qora.crypto.Crypto; +import qora.transaction.Transaction.ValidationResult; +import repository.AssetRepository; +import repository.DataException; +import repository.Repository; + +public class Payment { + + // Properties + private Repository repository; + + // Constructors + + public Payment(Repository repository) { + this.repository = repository; + } + + // Processing + + // Validate multiple payments + public ValidationResult isValid(byte[] senderPublicKey, List payments, BigDecimal fee, boolean isZeroAmountValid) throws DataException { + AssetRepository assetRepository = this.repository.getAssetRepository(); + + // Check fee is positive + if (fee.compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + // Total up payment amounts by assetId + Map amountsByAssetId = new HashMap(); + // Add transaction fee to start with + amountsByAssetId.put(Asset.QORA, fee); + + // Check payments, and calculate amount total by assetId + for (PaymentData paymentData : payments) { + // Check amount is positive + if (paymentData.getAmount().compareTo(BigDecimal.ZERO) < 0) + return ValidationResult.NEGATIVE_AMOUNT; + + // Optional zero-amount check + if (!isZeroAmountValid && paymentData.getAmount().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_AMOUNT; + + // Check recipient address is valid + if (!Crypto.isValidAddress(paymentData.getRecipient())) + return ValidationResult.INVALID_ADDRESS; + + // Check asset amount is integer if asset is not divisible + AssetData assetData = assetRepository.fromAssetId(paymentData.getAssetId()); + if (!assetData.getIsDivisible() && paymentData.getAmount().stripTrailingZeros().scale() > 0) + return ValidationResult.INVALID_AMOUNT; + + amountsByAssetId.compute(paymentData.getAssetId(), (assetId, amount) -> amount == null ? amount : amount.add(paymentData.getAmount())); + } + + // Check sender has enough of each asset + Account sender = new PublicKeyAccount(this.repository, senderPublicKey); + for (Entry pair : amountsByAssetId.entrySet()) + if (sender.getConfirmedBalance(pair.getKey()).compareTo(pair.getValue()) == -1) + return ValidationResult.NO_BALANCE; + + return ValidationResult.OK; + } + + public ValidationResult isValid(byte[] senderPublicKey, List payments, BigDecimal fee) throws DataException { + return isValid(senderPublicKey, payments, fee, false); + } + + // Single payment forms + public ValidationResult isValid(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, boolean isZeroAmountValid) throws DataException { + return isValid(senderPublicKey, Collections.singletonList(paymentData), fee); + } + + public ValidationResult isValid(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee) throws DataException { + return isValid(senderPublicKey, paymentData, fee, false); + } + + public void process(byte[] senderPublicKey, List payments, BigDecimal fee, byte[] signature) throws DataException { + Account sender = new PublicKeyAccount(this.repository, senderPublicKey); + + // Update sender's balance due to fee + sender.setConfirmedBalance(Asset.QORA, sender.getConfirmedBalance(Asset.QORA).subtract(fee)); + + // Update sender's reference + sender.setLastReference(signature); + + // Process all payments + for (PaymentData paymentData : payments) { + Account recipient = new Account(this.repository, paymentData.getRecipient()); + long assetId = paymentData.getAssetId(); + BigDecimal amount = paymentData.getAmount(); + + // Update sender's balance due to amount + sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(amount)); + + // Update recipient's balance + recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(amount)); + + // For QORA amounts only: if recipient has no reference yet, then this is their starting reference + if (assetId == Asset.QORA && recipient.getLastReference() == null) + recipient.setLastReference(signature); + } + } + + public void process(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, byte[] signature) throws DataException { + process(senderPublicKey, Collections.singletonList(paymentData), fee, signature); + } + + public void orphan(byte[] senderPublicKey, List payments, BigDecimal fee, byte[] signature, byte[] reference) throws DataException { + Account sender = new PublicKeyAccount(this.repository, senderPublicKey); + + // Update sender's balance due to fee + sender.setConfirmedBalance(Asset.QORA, sender.getConfirmedBalance(Asset.QORA).subtract(fee)); + + // Update sender's reference + sender.setLastReference(reference); + + for (PaymentData paymentData : payments) { + Account recipient = new Account(this.repository, paymentData.getRecipient()); + long assetId = paymentData.getAssetId(); + BigDecimal amount = paymentData.getAmount(); + + // Update sender's balance due to amount + sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(amount)); + + // Update recipient's balance + recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(amount)); + + /* + * 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 (assetId == Asset.QORA && Arrays.equals(recipient.getLastReference(), signature)) + recipient.setLastReference(null); + } + } + + public void orphan(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, byte[] signature, byte[] reference) throws DataException { + orphan(senderPublicKey, Collections.singletonList(paymentData), fee, signature, reference); + } + +} diff --git a/src/qora/transaction/CancelOrderTransaction.java b/src/qora/transaction/CancelOrderTransaction.java new file mode 100644 index 00000000..368addf2 --- /dev/null +++ b/src/qora/transaction/CancelOrderTransaction.java @@ -0,0 +1,113 @@ +package qora.transaction; + +import java.math.BigDecimal; +import java.util.Arrays; + +import data.assets.OrderData; +import data.transaction.CancelOrderTransactionData; +import data.transaction.TransactionData; +import qora.account.Account; +import qora.account.PublicKeyAccount; +import qora.assets.Asset; +import qora.assets.Order; +import qora.crypto.Crypto; +import repository.AssetRepository; +import repository.DataException; +import repository.Repository; + +public class CancelOrderTransaction extends Transaction { + + // Constructors + + public CancelOrderTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + CancelOrderTransactionData cancelOrderTransactionData = (CancelOrderTransactionData) this.transactionData; + AssetRepository assetRepository = this.repository.getAssetRepository(); + + // Check fee is positive + if (cancelOrderTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + // Check order even exists + OrderData orderData = assetRepository.fromOrderId(cancelOrderTransactionData.getOrderId()); + + if (orderData == null) + return ValidationResult.ORDER_DOES_NOT_EXIST; + + Account creator = new PublicKeyAccount(this.repository, cancelOrderTransactionData.getCreatorPublicKey()); + + // Check creator's public key results in valid address + if (!Crypto.isValidAddress(creator.getAddress())) + return ValidationResult.INVALID_ADDRESS; + + // Check creator's public key matches order's creator's public key + Account orderCreator = new PublicKeyAccount(this.repository, orderData.getCreatorPublicKey()); + if (!orderCreator.getAddress().equals(creator.getAddress())) + return ValidationResult.INVALID_ORDER_CREATOR; + + // Check creator has enough QORA for fee + if (creator.getConfirmedBalance(Asset.QORA).compareTo(cancelOrderTransactionData.getFee()) == -1) + return ValidationResult.NO_BALANCE; + + // Check reference is correct + if (!Arrays.equals(creator.getLastReference(), cancelOrderTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + return ValidationResult.OK; + } + + // PROCESS/ORPHAN + + @Override + public void process() throws DataException { + CancelOrderTransactionData cancelOrderTransactionData = (CancelOrderTransactionData) this.transactionData; + Account creator = new PublicKeyAccount(this.repository, cancelOrderTransactionData.getCreatorPublicKey()); + + // Save this transaction itself + this.repository.getTransactionRepository().save(this.transactionData); + + // Update creator's balance regarding fee + creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(cancelOrderTransactionData.getFee())); + + // Update creator's last reference + creator.setLastReference(cancelOrderTransactionData.getSignature()); + + // Mark Order as completed so no more trades can happen + OrderData orderData = this.repository.getAssetRepository().fromOrderId(cancelOrderTransactionData.getOrderId()); + Order order = new Order(this.repository, orderData); + order.cancel(); + + // Update creator's balance with unfulfilled amount + creator.setConfirmedBalance(orderData.getHaveAssetId(), creator.getConfirmedBalance(orderData.getHaveAssetId()).add(order.getAmountLeft())); + } + + @Override + public void orphan() throws DataException { + CancelOrderTransactionData cancelOrderTransactionData = (CancelOrderTransactionData) this.transactionData; + Account creator = new PublicKeyAccount(this.repository, cancelOrderTransactionData.getCreatorPublicKey()); + + // Save this transaction itself + this.repository.getTransactionRepository().delete(this.transactionData); + + // Update creator's balance regarding fee + creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(cancelOrderTransactionData.getFee())); + + // Update creator's last reference + creator.setLastReference(cancelOrderTransactionData.getReference()); + + // Unmark Order as completed so trades can happen again + OrderData orderData = this.repository.getAssetRepository().fromOrderId(cancelOrderTransactionData.getOrderId()); + Order order = new Order(this.repository, orderData); + order.reopen(); + + // Update creator's balance with unfulfilled amount + creator.setConfirmedBalance(orderData.getHaveAssetId(), creator.getConfirmedBalance(orderData.getHaveAssetId()).subtract(order.getAmountLeft())); + } + +} diff --git a/src/qora/transaction/MessageTransaction.java b/src/qora/transaction/MessageTransaction.java index 41069261..fff22925 100644 --- a/src/qora/transaction/MessageTransaction.java +++ b/src/qora/transaction/MessageTransaction.java @@ -1,15 +1,15 @@ package qora.transaction; -import java.math.BigDecimal; import java.util.Arrays; +import data.PaymentData; 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.crypto.Crypto; +import qora.payment.Payment; import repository.DataException; import repository.Repository; @@ -24,9 +24,12 @@ public class MessageTransaction extends Transaction { // Processing - public ValidationResult isValid() throws DataException { - // Lowest cost checks first + private PaymentData getPaymentData() { + MessageTransactionData messageTransactionData = (MessageTransactionData) this.transactionData; + return new PaymentData(messageTransactionData.getRecipient(), Asset.QORA, messageTransactionData.getAmount()); + } + public ValidationResult isValid() throws DataException { MessageTransactionData messageTransactionData = (MessageTransactionData) this.transactionData; // Are message transactions even allowed at this point? @@ -40,103 +43,39 @@ public class MessageTransaction extends Transaction { 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(messageTransactionData.getRecipient())) - return ValidationResult.INVALID_ADDRESS; - - if (messageTransactionData.getVersion() == 1) { - // Check amount is positive (V1) - 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 (messageTransactionData.getAmount().compareTo(BigDecimal.ZERO) < 0) - return ValidationResult.NEGATIVE_AMOUNT; - } - - // Check fee is positive - if (messageTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) - return ValidationResult.NEGATIVE_FEE; - // Check reference is correct 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) - long assetId = messageTransactionData.getAssetId(); - if (assetId != Asset.QORA && !this.repository.getAssetRepository().assetExists(assetId)) - return ValidationResult.ASSET_DOES_NOT_EXIST; + // Zero-amount payments (i.e. message-only) only valid for versions later than 1 + boolean isZeroAmountValid = messageTransactionData.getVersion() > 1; - // If asset is QORA then we need to check amount + fee in one go - if (assetId == Asset.QORA) { - // Check sender has enough funds for amount + fee in QORA - 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 (sender.getConfirmedBalance(assetId).compareTo(messageTransactionData.getAmount()) == -1) - return ValidationResult.NO_BALANCE; - - // Check sender has enough funds for fee in QORA - if (sender.getConfirmedBalance(Asset.QORA).compareTo(messageTransactionData.getFee()) == -1) - return ValidationResult.NO_BALANCE; - } - - return ValidationResult.OK; + // Wrap and delegate final payment checks to Payment class + return new Payment(this.repository).isValid(messageTransactionData.getSenderPublicKey(), getPaymentData(), messageTransactionData.getFee(), + isZeroAmountValid); } 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 - Account sender = new PublicKeyAccount(this.repository, messageTransactionData.getSenderPublicKey()); - sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(messageTransactionData.getAmount())); - // Update sender's balance due to fee - sender.setConfirmedBalance(Asset.QORA, sender.getConfirmedBalance(Asset.QORA).subtract(messageTransactionData.getFee())); - - // Update recipient's balance - Account recipient = new Account(this.repository, messageTransactionData.getRecipient()); - recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(messageTransactionData.getAmount())); - - // Update sender's reference - sender.setLastReference(messageTransactionData.getSignature()); - - // For QORA amounts only: if recipient has no reference yet, then this is their starting reference - if (assetId == Asset.QORA && recipient.getLastReference() == null) - recipient.setLastReference(messageTransactionData.getSignature()); + // Wrap and delegate payment processing to Payment class + new Payment(this.repository).process(messageTransactionData.getSenderPublicKey(), getPaymentData(), messageTransactionData.getFee(), + messageTransactionData.getSignature()); } 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 - Account sender = new PublicKeyAccount(this.repository, messageTransactionData.getSenderPublicKey()); - sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(messageTransactionData.getAmount())); - // Update sender's balance due to fee - sender.setConfirmedBalance(Asset.QORA, sender.getConfirmedBalance(Asset.QORA).add(messageTransactionData.getFee())); - - // Update recipient's balance - Account recipient = new Account(this.repository, messageTransactionData.getRecipient()); - recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(messageTransactionData.getAmount())); - - // Update sender's 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 (assetId == Asset.QORA && Arrays.equals(recipient.getLastReference(), messageTransactionData.getSignature())) - recipient.setLastReference(null); + // Wrap and delegate payment processing to Payment class + new Payment(this.repository).orphan(messageTransactionData.getSenderPublicKey(), getPaymentData(), messageTransactionData.getFee(), + messageTransactionData.getSignature(), messageTransactionData.getReference()); } } diff --git a/src/qora/transaction/MultiPaymentTransaction.java b/src/qora/transaction/MultiPaymentTransaction.java new file mode 100644 index 00000000..30b3b996 --- /dev/null +++ b/src/qora/transaction/MultiPaymentTransaction.java @@ -0,0 +1,81 @@ +package qora.transaction; + +import java.util.Arrays; +import java.util.List; + +import data.PaymentData; +import data.transaction.MultiPaymentTransactionData; +import data.transaction.TransactionData; +import qora.account.PublicKeyAccount; +import qora.assets.Asset; +import qora.block.Block; +import qora.payment.Payment; +import repository.DataException; +import repository.Repository; +import utils.NTP; + +public class MultiPaymentTransaction extends Transaction { + + private static final int MAX_PAYMENTS_COUNT = 400; + + // Constructors + + public MultiPaymentTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + } + + @Override + public ValidationResult isValid() throws DataException { + MultiPaymentTransactionData multiPaymentTransactionData = (MultiPaymentTransactionData) this.transactionData; + List payments = multiPaymentTransactionData.getPayments(); + + // Are MultiPaymentTransactions even allowed at this point? + if (NTP.getTime() < Block.ASSETS_RELEASE_TIMESTAMP) + return ValidationResult.NOT_YET_RELEASED; + + // Check number of payments + if (payments.size() < 1 || payments.size() > MAX_PAYMENTS_COUNT) + return ValidationResult.INVALID_PAYMENTS_COUNT; + + // Check reference is correct + PublicKeyAccount sender = new PublicKeyAccount(this.repository, multiPaymentTransactionData.getSenderPublicKey()); + + if (!Arrays.equals(sender.getLastReference(), multiPaymentTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + // Check sender has enough funds for fee + // NOTE: in Gen1 pre-POWFIX-RELEASE transactions didn't have this check + if (multiPaymentTransactionData.getTimestamp() >= Block.POWFIX_RELEASE_TIMESTAMP + && sender.getConfirmedBalance(Asset.QORA).compareTo(multiPaymentTransactionData.getFee()) == -1) + return ValidationResult.NO_BALANCE; + + return new Payment(this.repository).isValid(multiPaymentTransactionData.getSenderPublicKey(), payments, multiPaymentTransactionData.getFee()); + } + + // PROCESS/ORPHAN + + @Override + public void process() throws DataException { + MultiPaymentTransactionData multiPaymentTransactionData = (MultiPaymentTransactionData) this.transactionData; + + // Save this transaction itself + this.repository.getTransactionRepository().save(this.transactionData); + + // Wrap and delegate payment processing to Payment class + new Payment(this.repository).process(multiPaymentTransactionData.getSenderPublicKey(), multiPaymentTransactionData.getPayments(), + multiPaymentTransactionData.getFee(), multiPaymentTransactionData.getSignature()); + } + + @Override + public void orphan() throws DataException { + MultiPaymentTransactionData multiPaymentTransactionData = (MultiPaymentTransactionData) this.transactionData; + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(this.transactionData); + + // Wrap and delegate payment processing to Payment class + new Payment(this.repository).orphan(multiPaymentTransactionData.getSenderPublicKey(), multiPaymentTransactionData.getPayments(), + multiPaymentTransactionData.getFee(), multiPaymentTransactionData.getSignature(), multiPaymentTransactionData.getReference()); + } + +} diff --git a/src/qora/transaction/PaymentTransaction.java b/src/qora/transaction/PaymentTransaction.java index 1dbd841f..28d24a86 100644 --- a/src/qora/transaction/PaymentTransaction.java +++ b/src/qora/transaction/PaymentTransaction.java @@ -1,14 +1,14 @@ package qora.transaction; -import java.math.BigDecimal; import java.util.Arrays; +import data.PaymentData; 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 qora.payment.Payment; import repository.DataException; import repository.Repository; @@ -22,33 +22,21 @@ public class PaymentTransaction extends Transaction { // Processing - public ValidationResult isValid() throws DataException { - // Lowest cost checks first - + private PaymentData getPaymentData() { PaymentTransactionData paymentTransactionData = (PaymentTransactionData) this.transactionData; + return new PaymentData(paymentTransactionData.getRecipient(), Asset.QORA, paymentTransactionData.getAmount()); + } - // Check recipient is a valid address - if (!Crypto.isValidAddress(paymentTransactionData.getRecipient())) - return ValidationResult.INVALID_ADDRESS; - - // Check amount is positive - if (paymentTransactionData.getAmount().compareTo(BigDecimal.ZERO) <= 0) - return ValidationResult.NEGATIVE_AMOUNT; - - // Check fee is positive - if (paymentTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) - return ValidationResult.NEGATIVE_FEE; + public ValidationResult isValid() throws DataException { + PaymentTransactionData paymentTransactionData = (PaymentTransactionData) this.transactionData; // Check reference is correct Account sender = new PublicKeyAccount(repository, paymentTransactionData.getSenderPublicKey()); if (!Arrays.equals(sender.getLastReference(), paymentTransactionData.getReference())) return ValidationResult.INVALID_REFERENCE; - // Check sender has enough funds - if (sender.getConfirmedBalance(Asset.QORA).compareTo(paymentTransactionData.getAmount().add(paymentTransactionData.getFee())) == -1) - return ValidationResult.NO_BALANCE; - - return ValidationResult.OK; + // Wrap and delegate final payment checks to Payment class + return new Payment(this.repository).isValid(paymentTransactionData.getSenderPublicKey(), getPaymentData(), paymentTransactionData.getFee()); } public void process() throws DataException { @@ -57,21 +45,9 @@ public class PaymentTransaction extends Transaction { // Save this transaction itself this.repository.getTransactionRepository().save(this.transactionData); - // Update sender's balance - 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 - Account recipient = new Account(repository, paymentTransactionData.getRecipient()); - recipient.setConfirmedBalance(Asset.QORA, recipient.getConfirmedBalance(Asset.QORA).add(paymentTransactionData.getAmount())); - - // Update sender's reference - sender.setLastReference(paymentTransactionData.getSignature()); - - // If recipient has no reference yet, then this is their starting reference - if (recipient.getLastReference() == null) - recipient.setLastReference(paymentTransactionData.getSignature()); + // Wrap and delegate payment processing to Payment class + new Payment(this.repository).process(paymentTransactionData.getSenderPublicKey(), getPaymentData(), paymentTransactionData.getFee(), + paymentTransactionData.getSignature()); } public void orphan() throws DataException { @@ -80,24 +56,9 @@ public class PaymentTransaction extends Transaction { // Delete this transaction this.repository.getTransactionRepository().delete(this.transactionData); - // Update sender's balance - 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 - Account recipient = new Account(repository, paymentTransactionData.getRecipient()); - recipient.setConfirmedBalance(Asset.QORA, recipient.getConfirmedBalance(Asset.QORA).subtract(paymentTransactionData.getAmount())); - - // Update sender's 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(recipient.getLastReference(), paymentTransactionData.getSignature())) - recipient.setLastReference(null); + // Wrap and delegate payment processing to Payment class + new Payment(this.repository).orphan(paymentTransactionData.getSenderPublicKey(), getPaymentData(), paymentTransactionData.getFee(), + paymentTransactionData.getSignature(), paymentTransactionData.getReference()); } } diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index 67f69c1e..f524deef 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -41,8 +41,10 @@ public abstract class Transaction { // Validation results public enum ValidationResult { - OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6), INVALID_NAME_LENGTH(7), INVALID_AMOUNT(15), INVALID_DESCRIPTION_LENGTH( - 18), INVALID_DATA_LENGTH(27), INVALID_QUANTITY(28), ASSET_DOES_NOT_EXIST(29), INVALID_RETURN(30), HAVE_EQUALS_WANT(31), NEGATIVE_PRICE(35), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000); + OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6), INVALID_NAME_LENGTH(7), INVALID_AMOUNT( + 15), INVALID_DESCRIPTION_LENGTH(18), INVALID_DATA_LENGTH(27), INVALID_QUANTITY(28), ASSET_DOES_NOT_EXIST(29), INVALID_RETURN( + 30), HAVE_EQUALS_WANT(31), ORDER_DOES_NOT_EXIST(32), INVALID_ORDER_CREATOR( + 33), INVALID_PAYMENTS_COUNT(34), NEGATIVE_PRICE(35), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000); public final int value; diff --git a/src/qora/transaction/TransferAssetTransaction.java b/src/qora/transaction/TransferAssetTransaction.java index 101c8407..9e72e490 100644 --- a/src/qora/transaction/TransferAssetTransaction.java +++ b/src/qora/transaction/TransferAssetTransaction.java @@ -1,18 +1,14 @@ package qora.transaction; -import java.math.BigDecimal; import java.util.Arrays; -import data.assets.AssetData; +import data.PaymentData; import data.transaction.TransactionData; import data.transaction.TransferAssetTransactionData; import utils.NTP; -import qora.account.Account; import qora.account.PublicKeyAccount; -import qora.assets.Asset; import qora.block.Block; -import qora.crypto.Crypto; -import repository.AssetRepository; +import qora.payment.Payment; import repository.DataException; import repository.Repository; @@ -26,60 +22,28 @@ public class TransferAssetTransaction extends Transaction { // Processing + private PaymentData getPaymentData() { + TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) this.transactionData; + return new PaymentData(transferAssetTransactionData.getRecipient(), transferAssetTransactionData.getAssetId(), + transferAssetTransactionData.getAmount()); + } + @Override public ValidationResult isValid() throws DataException { - // Lowest cost checks first - TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) this.transactionData; // Are IssueAssetTransactions even allowed at this point? if (NTP.getTime() < Block.ASSETS_RELEASE_TIMESTAMP) return ValidationResult.NOT_YET_RELEASED; - // Check recipient address is valid - if (!Crypto.isValidAddress(transferAssetTransactionData.getRecipient())) - return ValidationResult.INVALID_ADDRESS; - - // Check amount is positive - if (transferAssetTransactionData.getAmount().compareTo(BigDecimal.ZERO) <= 0) - return ValidationResult.NEGATIVE_FEE; - - // Check fee is positive - if (transferAssetTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) - return ValidationResult.NEGATIVE_FEE; - // Check reference is correct PublicKeyAccount sender = new PublicKeyAccount(this.repository, transferAssetTransactionData.getSenderPublicKey()); if (!Arrays.equals(sender.getLastReference(), transferAssetTransactionData.getReference())) return ValidationResult.INVALID_REFERENCE; - // Check sender has enough asset balance AFTER removing fee, in case asset is QORA - long assetId = transferAssetTransactionData.getAssetId(); - // If asset is QORA then we need to check amount + fee in one go - if (assetId == Asset.QORA) { - // Check sender has enough funds for amount + fee in QORA - if (sender.getConfirmedBalance(Asset.QORA).compareTo(transferAssetTransactionData.getAmount().add(transferAssetTransactionData.getFee())) == -1) - return ValidationResult.NO_BALANCE; - } else { - // Check sender has enough funds for amount in whatever asset - if (sender.getConfirmedBalance(assetId).compareTo(transferAssetTransactionData.getAmount()) == -1) - return ValidationResult.NO_BALANCE; - - // Check sender has enough funds for fee in QORA - // NOTE: in Gen1 pre-POWFIX-RELEASE transactions didn't have this check - if (transferAssetTransactionData.getTimestamp() >= Block.POWFIX_RELEASE_TIMESTAMP - && sender.getConfirmedBalance(Asset.QORA).compareTo(transferAssetTransactionData.getFee()) == -1) - return ValidationResult.NO_BALANCE; - } - - // Check asset amount is integer if asset is not divisible - AssetRepository assetRepository = this.repository.getAssetRepository(); - AssetData assetData = assetRepository.fromAssetId(assetId); - if (!assetData.getIsDivisible() && transferAssetTransactionData.getAmount().stripTrailingZeros().scale() > 0) - return ValidationResult.INVALID_AMOUNT; - - return ValidationResult.OK; + // Wrap asset transfer as a payment and delegate final payment checks to Payment class + return new Payment(this.repository).isValid(transferAssetTransactionData.getSenderPublicKey(), getPaymentData(), transferAssetTransactionData.getFee()); } // PROCESS/ORPHAN @@ -87,56 +51,25 @@ public class TransferAssetTransaction extends Transaction { @Override public void process() throws DataException { TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) this.transactionData; - long assetId = transferAssetTransactionData.getAssetId(); // Save this transaction itself this.repository.getTransactionRepository().save(this.transactionData); - // Update sender's balance due to amount - Account sender = new PublicKeyAccount(this.repository, transferAssetTransactionData.getSenderPublicKey()); - sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(transferAssetTransactionData.getAmount())); - // Update sender's balance due to fee - sender.setConfirmedBalance(Asset.QORA, sender.getConfirmedBalance(Asset.QORA).subtract(transferAssetTransactionData.getFee())); - - // Update recipient's balance - Account recipient = new Account(this.repository, transferAssetTransactionData.getRecipient()); - recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(transferAssetTransactionData.getAmount())); - - // Update sender's reference - sender.setLastReference(transferAssetTransactionData.getSignature()); - - // For QORA amounts only: if recipient has no reference yet, then this is their starting reference - if (assetId == Asset.QORA && recipient.getLastReference() == null) - recipient.setLastReference(transferAssetTransactionData.getSignature()); + // Wrap asset transfer as a payment and delegate processing to Payment class + new Payment(this.repository).process(transferAssetTransactionData.getSenderPublicKey(), getPaymentData(), transferAssetTransactionData.getFee(), + transferAssetTransactionData.getSignature()); } @Override public void orphan() throws DataException { TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) this.transactionData; - long assetId = transferAssetTransactionData.getAssetId(); // Delete this transaction itself this.repository.getTransactionRepository().delete(this.transactionData); - // Update sender's balance due to amount - Account sender = new PublicKeyAccount(this.repository, transferAssetTransactionData.getSenderPublicKey()); - sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(transferAssetTransactionData.getAmount())); - // Update sender's balance due to fee - sender.setConfirmedBalance(Asset.QORA, sender.getConfirmedBalance(Asset.QORA).add(transferAssetTransactionData.getFee())); - - // Update recipient's balance - Account recipient = new Account(this.repository, transferAssetTransactionData.getRecipient()); - recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(transferAssetTransactionData.getAmount())); - - // Update sender's reference - sender.setLastReference(transferAssetTransactionData.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 (assetId == Asset.QORA && Arrays.equals(recipient.getLastReference(), transferAssetTransactionData.getSignature())) - recipient.setLastReference(null); + // Wrap asset transfer as a payment and delegate processing to Payment class + new Payment(this.repository).orphan(transferAssetTransactionData.getSenderPublicKey(), getPaymentData(), transferAssetTransactionData.getFee(), + transferAssetTransactionData.getSignature(), transferAssetTransactionData.getReference()); } } diff --git a/src/repository/AssetRepository.java b/src/repository/AssetRepository.java index 94a7a9b7..66ff6745 100644 --- a/src/repository/AssetRepository.java +++ b/src/repository/AssetRepository.java @@ -21,4 +21,8 @@ public interface AssetRepository { public OrderData fromOrderId(byte[] orderId) throws DataException; + public void save(OrderData orderData) throws DataException; + + public void delete(byte[] orderId) throws DataException; + } diff --git a/src/repository/hsqldb/HSQLDBAssetRepository.java b/src/repository/hsqldb/HSQLDBAssetRepository.java index 8212bcec..a877eaf3 100644 --- a/src/repository/hsqldb/HSQLDBAssetRepository.java +++ b/src/repository/hsqldb/HSQLDBAssetRepository.java @@ -84,7 +84,8 @@ public class HSQLDBAssetRepository implements AssetRepository { public OrderData fromOrderId(byte[] orderId) throws DataException { try { ResultSet resultSet = this.repository.checkedExecute( - "SELECT creator, have_asset_id, want_asset_id, amount, fulfilled, price, timestamp FROM AssetOrders WHERE asset_order_id = ?", orderId); + "SELECT creator, have_asset_id, want_asset_id, amount, fulfilled, price, timestamp, is_closed FROM AssetOrders WHERE asset_order_id = ?", + orderId); if (resultSet == null) return null; @@ -95,10 +96,32 @@ public class HSQLDBAssetRepository implements AssetRepository { BigDecimal fulfilled = resultSet.getBigDecimal(5); BigDecimal price = resultSet.getBigDecimal(6); long timestamp = resultSet.getTimestamp(7).getTime(); + boolean isClosed = resultSet.getBoolean(8); - return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp); + return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed); } catch (SQLException e) { - throw new DataException("Unable to fetch order from repository", e); + throw new DataException("Unable to fetch asset order from repository", e); + } + } + + public void save(OrderData orderData) throws DataException { + HSQLDBSaver saveHelper = new HSQLDBSaver("AssetOrders"); + saveHelper.bind("asset_order_id", orderData.getOrderId()).bind("creator", orderData.getCreatorPublicKey()) + .bind("have_asset_id", orderData.getHaveAssetId()).bind("want_asset_id", orderData.getWantAssetId()).bind("amount", orderData.getAmount()) + .bind("fulfilled", orderData.getFulfilled()).bind("price", orderData.getPrice()).bind("isClosed", orderData.getIsClosed()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save asset order into repository", e); + } + } + + public void delete(byte[] orderId) throws DataException { + try { + this.repository.checkedExecute("DELETE FROM AssetOrders WHERE orderId = ?", orderId); + } catch (SQLException e) { + throw new DataException("Unable to delete asset order from repository", e); } } diff --git a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java index 8ce732eb..d88acc92 100644 --- a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -286,10 +286,10 @@ public class HSQLDBDatabaseUpdates { // Asset Orders stmt.execute( "CREATE TABLE AssetOrders (asset_order_id AssetOrderID, creator QoraPublicKey NOT NULL, have_asset_id AssetID NOT NULL, want_asset_id AssetID NOT NULL, " - + "amount QoraAmount NOT NULL, fulfilled QoraAmount NOT NULL, price QoraAmount NOT NULL, ordered TIMESTAMP NOT NULL, " + + "amount QoraAmount NOT NULL, fulfilled QoraAmount NOT NULL, price QoraAmount NOT NULL, ordered TIMESTAMP NOT NULL, is_closed BOOLEAN NOT NULL, " + "PRIMARY KEY (asset_order_id))"); - stmt.execute("CREATE INDEX AssetOrderHaveIndex on AssetOrders (have_asset_id)"); - stmt.execute("CREATE INDEX AssetOrderWantIndex on AssetOrders (want_asset_id)"); + stmt.execute("CREATE INDEX AssetOrderHaveIndex on AssetOrders (have_asset_id, is_closed)"); + stmt.execute("CREATE INDEX AssetOrderWantIndex on AssetOrders (want_asset_id, is_closed)"); break; default: diff --git a/src/transform/PaymentTransformer.java b/src/transform/PaymentTransformer.java new file mode 100644 index 00000000..6c197e55 --- /dev/null +++ b/src/transform/PaymentTransformer.java @@ -0,0 +1,75 @@ +package transform; + +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.primitives.Longs; + +import data.PaymentData; +import transform.TransformationException; +import utils.Base58; +import utils.Serialization; + +public class PaymentTransformer extends Transformer { + + // Property lengths + private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH; + private static final int ASSET_ID_LENGTH = LONG_LENGTH; + private static final int AMOUNT_LENGTH = BIG_DECIMAL_LENGTH; + + private static final int TOTAL_LENGTH = RECIPIENT_LENGTH + ASSET_ID_LENGTH + AMOUNT_LENGTH; + + public static PaymentData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + if (byteBuffer.remaining() < TOTAL_LENGTH) + throw new TransformationException("Byte data too short for payment information"); + + String recipient = Serialization.deserializeRecipient(byteBuffer); + long assetId = byteBuffer.getLong(); + BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); + + return new PaymentData(recipient, assetId, amount); + } + + public static int getDataLength() throws TransformationException { + return TOTAL_LENGTH; + } + + public static byte[] toBytes(PaymentData paymentData) throws TransformationException { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Base58.decode(paymentData.getRecipient())); + bytes.write(Longs.toByteArray(paymentData.getAssetId())); + Serialization.serializeBigDecimal(bytes, paymentData.getAmount()); + + return bytes.toByteArray(); + } catch (IOException | ClassCastException e) { + throw new TransformationException(e); + } + } + + @SuppressWarnings("unchecked") + public static JSONObject toJSON(PaymentData paymentData) throws TransformationException { + JSONObject json = new JSONObject(); + + try { + json.put("recipient", paymentData.getRecipient()); + + // For gen1 support: + json.put("asset", paymentData.getAssetId()); + // Gen2 version: + json.put("assetId", paymentData.getAssetId()); + + json.put("amount", paymentData.getAmount().toPlainString()); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/transform/transaction/CancelOrderTransactionTransformer.java b/src/transform/transaction/CancelOrderTransactionTransformer.java new file mode 100644 index 00000000..85de3624 --- /dev/null +++ b/src/transform/transaction/CancelOrderTransactionTransformer.java @@ -0,0 +1,97 @@ +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.CancelOrderTransactionData; +import transform.TransformationException; +import utils.Base58; +import utils.Serialization; + +public class CancelOrderTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int CREATOR_LENGTH = LONG_LENGTH; + private static final int ORDER_ID_LENGTH = SIGNATURE_LENGTH; + + private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + CREATOR_LENGTH + ORDER_ID_LENGTH; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + if (byteBuffer.remaining() < TYPELESS_LENGTH) + throw new TransformationException("Byte data too short for CancelOrderTransaction"); + + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] creatorPublicKey = Serialization.deserializePublicKey(byteBuffer); + + byte[] orderId = new byte[ORDER_ID_LENGTH]; + byteBuffer.get(orderId); + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new CancelOrderTransactionData(creatorPublicKey, orderId, 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 { + CancelOrderTransactionData cancelOrderTransactionData = (CancelOrderTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(cancelOrderTransactionData.getType().value)); + bytes.write(Longs.toByteArray(cancelOrderTransactionData.getTimestamp())); + bytes.write(cancelOrderTransactionData.getReference()); + + bytes.write(cancelOrderTransactionData.getCreatorPublicKey()); + bytes.write(cancelOrderTransactionData.getOrderId()); + + Serialization.serializeBigDecimal(bytes, cancelOrderTransactionData.getFee()); + bytes.write(cancelOrderTransactionData.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 { + CancelOrderTransactionData cancelOrderTransactionData = (CancelOrderTransactionData) transactionData; + + byte[] creatorPublicKey = cancelOrderTransactionData.getCreatorPublicKey(); + + json.put("creator", PublicKeyAccount.getAddress(creatorPublicKey)); + json.put("creatorPublicKey", HashCode.fromBytes(creatorPublicKey).toString()); + + json.put("order", Base58.encode(cancelOrderTransactionData.getOrderId())); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/transform/transaction/MultiPaymentTransactionTransformer.java b/src/transform/transaction/MultiPaymentTransactionTransformer.java new file mode 100644 index 00000000..bb4307e6 --- /dev/null +++ b/src/transform/transaction/MultiPaymentTransactionTransformer.java @@ -0,0 +1,121 @@ +package transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.json.simple.JSONArray; +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.PaymentData; +import data.transaction.MultiPaymentTransactionData; +import transform.PaymentTransformer; +import transform.TransformationException; +import utils.Serialization; + +public class MultiPaymentTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int SENDER_LENGTH = PUBLIC_KEY_LENGTH; + private static final int PAYMENTS_COUNT_LENGTH = INT_LENGTH; + + private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + PAYMENTS_COUNT_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); + int paymentsCount = byteBuffer.getInt(); + + // Check remaining buffer size + int minRemaining = paymentsCount * PaymentTransformer.getDataLength() + FEE_LENGTH + SIGNATURE_LENGTH; + if (byteBuffer.remaining() < minRemaining) + throw new TransformationException("Byte data too short for PaymentTransaction"); + + List payments = new ArrayList(); + for (int i = 0; i < paymentsCount; ++i) + payments.add(PaymentTransformer.fromByteBuffer(byteBuffer)); + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new MultiPaymentTransactionData(senderPublicKey, payments, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + MultiPaymentTransactionData multiPaymentTransactionData = (MultiPaymentTransactionData) transactionData; + + return TYPE_LENGTH + TYPELESS_LENGTH + multiPaymentTransactionData.getPayments().size() * PaymentTransformer.getDataLength(); + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + MultiPaymentTransactionData multiPaymentTransactionData = (MultiPaymentTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(multiPaymentTransactionData.getType().value)); + bytes.write(Longs.toByteArray(multiPaymentTransactionData.getTimestamp())); + bytes.write(multiPaymentTransactionData.getReference()); + + bytes.write(multiPaymentTransactionData.getSenderPublicKey()); + + List payments = multiPaymentTransactionData.getPayments(); + bytes.write(Ints.toByteArray(payments.size())); + + for (PaymentData paymentData : payments) + PaymentTransformer.toBytes(paymentData); + + Serialization.serializeBigDecimal(bytes, multiPaymentTransactionData.getFee()); + bytes.write(multiPaymentTransactionData.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 { + MultiPaymentTransactionData multiPaymentTransactionData = (MultiPaymentTransactionData) transactionData; + + byte[] senderPublicKey = multiPaymentTransactionData.getSenderPublicKey(); + + json.put("sender", PublicKeyAccount.getAddress(senderPublicKey)); + json.put("senderPublicKey", HashCode.fromBytes(senderPublicKey).toString()); + + List payments = multiPaymentTransactionData.getPayments(); + JSONArray paymentsJson = new JSONArray(); + + for (PaymentData paymentData : payments) + paymentsJson.add(PaymentTransformer.toJSON(paymentData)); + + json.put("payments", paymentsJson); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +}