diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java index 6b573d2b..d32071db 100644 --- a/src/qora/transaction/GenesisTransaction.java +++ b/src/qora/transaction/GenesisTransaction.java @@ -3,6 +3,7 @@ package qora.transaction; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigDecimal; +import java.nio.ByteBuffer; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -31,7 +32,7 @@ public class GenesisTransaction extends Transaction { private BigDecimal amount; // Property lengths - private static final int RECIPIENT_LENGTH = 32; + private static final int RECIPIENT_LENGTH = 25; // raw, not Base58-encoded private static final int AMOUNT_LENGTH = 8; // Note that Genesis transactions don't require reference, fee or signature: private static final int TYPELESS_LENGTH = TIMESTAMP_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH; @@ -110,9 +111,15 @@ public class GenesisTransaction extends Transaction { // Converters - public static Transaction parse(byte[] data) throws Exception { - // TODO - return null; + protected static Transaction parse(ByteBuffer byteBuffer) throws TransactionParseException { + if (byteBuffer.remaining() < TYPELESS_LENGTH) + throw new TransactionParseException("Byte data too short for GenesisTransaction"); + + long timestamp = byteBuffer.getLong(); + String recipient = Serialization.deserializeRecipient(byteBuffer); + BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); + + return new GenesisTransaction(recipient, amount, timestamp); } @SuppressWarnings("unchecked") diff --git a/src/qora/transaction/PaymentTransaction.java b/src/qora/transaction/PaymentTransaction.java index b8144008..4a99da96 100644 --- a/src/qora/transaction/PaymentTransaction.java +++ b/src/qora/transaction/PaymentTransaction.java @@ -3,6 +3,7 @@ package qora.transaction; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigDecimal; +import java.nio.ByteBuffer; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -30,7 +31,6 @@ public class PaymentTransaction extends Transaction { // Property lengths private static final int SENDER_LENGTH = 32; - private static final int RECIPIENT_LENGTH = 32; private static final int AMOUNT_LENGTH = 8; private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH; @@ -118,9 +118,22 @@ public class PaymentTransaction extends Transaction { // Converters - public static Transaction parse(byte[] data) throws Exception { + protected static Transaction parse(ByteBuffer byteBuffer) throws TransactionParseException { // TODO - return null; + if (byteBuffer.remaining() < TYPELESS_LENGTH) + throw new TransactionParseException("Byte data too short for PaymentTransaction"); + + long timestamp = byteBuffer.getLong(); + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + PublicKeyAccount sender = Serialization.deserializePublicKey(byteBuffer); + String recipient = Serialization.deserializeRecipient(byteBuffer); + BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new PaymentTransaction(sender, recipient, amount, fee, timestamp, reference, signature); } @SuppressWarnings("unchecked") diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index 2902e72e..4df98c24 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -2,6 +2,7 @@ package qora.transaction; import java.math.BigDecimal; import java.math.MathContext; +import java.nio.ByteBuffer; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -89,7 +90,8 @@ public abstract class Transaction { protected static final int BASE_TYPELESS_LENGTH = TIMESTAMP_LENGTH + REFERENCE_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH; // Other length constants - protected static final int CREATOR_LENGTH = 32; + public static final int CREATOR_LENGTH = 32; + public static final int RECIPIENT_LENGTH = 25; // Constructors @@ -283,6 +285,31 @@ public abstract class Transaction { // Converters + public static Transaction parse(byte[] data) throws TransactionParseException { + if (data == null) + return null; + + if (data.length < TYPE_LENGTH) + throw new TransactionParseException("Byte data too short to determine transaction type"); + + ByteBuffer byteBuffer = ByteBuffer.wrap(data); + + TransactionType type = TransactionType.valueOf(byteBuffer.getInt()); + if (type == null) + return null; + + switch (type) { + case GENESIS: + return GenesisTransaction.parse(byteBuffer); + + case PAYMENT: + return PaymentTransaction.parse(byteBuffer); + + default: + return null; + } + } + public abstract JSONObject toJSON() throws SQLException; /** diff --git a/src/qora/transaction/TransactionParseException.java b/src/qora/transaction/TransactionParseException.java new file mode 100644 index 00000000..a0fd4a1a --- /dev/null +++ b/src/qora/transaction/TransactionParseException.java @@ -0,0 +1,10 @@ +package qora.transaction; + +@SuppressWarnings("serial") +public class TransactionParseException extends Exception { + + public TransactionParseException(String message) { + super(message); + } + +} diff --git a/src/test/transactions.java b/src/test/transactions.java new file mode 100644 index 00000000..8a56c2c0 --- /dev/null +++ b/src/test/transactions.java @@ -0,0 +1,64 @@ +package test; + +import static org.junit.Assert.*; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +import database.DB; +import qora.block.Block; +import qora.block.GenesisBlock; +import qora.transaction.GenesisTransaction; +import qora.transaction.Transaction; +import qora.transaction.TransactionParseException; + +public class transactions extends common { + + @Test + public void testGenesisSerialization() throws SQLException, TransactionParseException { + GenesisBlock block = GenesisBlock.getInstance(); + + GenesisTransaction transaction = (GenesisTransaction) block.getTransactions().get(1); + assertNotNull(transaction); + System.out.println(transaction.getTimestamp() + ": " + transaction.getRecipient().getAddress() + " received " + transaction.getAmount().toPlainString()); + + byte[] bytes = transaction.toBytes(); + + GenesisTransaction parsedTransaction = (GenesisTransaction) Transaction.parse(bytes); + System.out.println(parsedTransaction.getTimestamp() + ": " + parsedTransaction.getRecipient().getAddress() + " received " + parsedTransaction.getAmount().toPlainString()); + + assertTrue(Arrays.equals(transaction.getSignature(), parsedTransaction.getSignature())); + } + + public void testGenericSerialization(Transaction transaction) throws SQLException, TransactionParseException { + assertNotNull(transaction); + + byte[] bytes = transaction.toBytes(); + + Transaction parsedTransaction = Transaction.parse(bytes); + + assertTrue(Arrays.equals(transaction.getSignature(), parsedTransaction.getSignature())); + } + + @Test + public void testPaymentSerialization() throws SQLException, TransactionParseException { + try (final Connection connection = DB.getConnection()) { + // Block 949 has lots of varied transactions + // Blocks 390 & 754 have only payment transactions + Block block = Block.fromHeight(754); + assertNotNull("Block 754 is required for this test", block); + assertTrue(block.isSignatureValid()); + + List transactions = block.getTransactions(); + assertNotNull(transactions); + + for (Transaction transaction : transactions) + testGenericSerialization(transaction); + } + } + +} \ No newline at end of file diff --git a/src/utils/Serialization.java b/src/utils/Serialization.java index 77676ba1..0dda5bdb 100644 --- a/src/utils/Serialization.java +++ b/src/utils/Serialization.java @@ -1,6 +1,11 @@ package utils; import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; + +import qora.account.PublicKeyAccount; +import qora.transaction.Transaction; public class Serialization { @@ -17,4 +22,22 @@ public class Serialization { return output; } + public static BigDecimal deserializeBigDecimal(ByteBuffer byteBuffer) { + byte[] bytes = new byte[8]; + byteBuffer.get(bytes); + return new BigDecimal(new BigInteger(bytes), 8); + } + + public static String deserializeRecipient(ByteBuffer byteBuffer) { + byte[] bytes = new byte[Transaction.RECIPIENT_LENGTH]; + byteBuffer.get(bytes); + return Base58.encode(bytes); + } + + public static PublicKeyAccount deserializePublicKey(ByteBuffer byteBuffer) { + byte[] bytes = new byte[Transaction.CREATOR_LENGTH]; + byteBuffer.get(bytes); + return new PublicKeyAccount(bytes); + } + }