Work on assets/payments

* Created PaymentData transfer objects for (recipient, assetId, amount) tuples
* Created corresponding Payment class for validating, processing and orphaning payment(s)

* Modified OrderData to support isClosed for when an Order is cancelled so no more trades can occur

* Migrated CancelOrderTransactions and MultiPaymentTransactions

* Converted MessageTransactions, PaymentTransactions and TransferAssetTransactions to use new Payment class

Can't use PaymentTransformer in PaymentTransformer or TransferAssetTransformer due to serialization differences.
This commit is contained in:
catbref 2018-06-14 16:46:55 +01:00
parent 2f8c160627
commit 9897981de1
18 changed files with 861 additions and 226 deletions

34
src/data/PaymentData.java Normal file
View File

@ -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;
}
}

View File

@ -12,9 +12,10 @@ public class OrderData implements Comparable<OrderData> {
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<OrderData> {
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<OrderData> {
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

View File

@ -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;
}
}

View File

@ -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<PaymentData> payments;
// Constructors
public MultiPaymentTransactionData(byte[] senderPublicKey, List<PaymentData> 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<PaymentData>(), fee, timestamp, reference, null);
}
// Getters/setters
public byte[] getSenderPublicKey() {
return this.senderPublicKey;
}
public List<PaymentData> getPayments() {
return this.payments;
}
}

View File

@ -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
}
}

View File

@ -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<PaymentData> 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<Long, BigDecimal> amountsByAssetId = new HashMap<Long, BigDecimal>();
// 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<Long, BigDecimal> 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<PaymentData> 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<PaymentData> 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<PaymentData> 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);
}
}

View File

@ -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()));
}
}

View File

@ -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());
}
}

View File

@ -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<PaymentData> 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());
}
}

View File

@ -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());
}
}

View File

@ -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;

View File

@ -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());
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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:

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<PaymentData> payments = new ArrayList<PaymentData>();
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<PaymentData> 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<PaymentData> 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;
}
}