Merge branch 'message-wo-recipient' into launch

This commit is contained in:
catbref 2020-05-18 10:12:54 +01:00
commit a7b9215ace
10 changed files with 273 additions and 47 deletions

View File

@ -815,9 +815,10 @@ public class CrossChainResource {
Long fee = null;
long amount = 0L;
Long assetId = null; // no assetId as amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, senderPublicKey, fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, 4, atAddress, Asset.QORT, amount, messageData, false, false);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, 4, atAddress, amount, assetId, messageData, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);

View File

@ -5,7 +5,6 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.asset.Asset;
import org.qortal.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
@ -16,14 +15,24 @@ import io.swagger.v3.oas.annotations.media.Schema;
public class MessageTransactionData extends TransactionData {
// Properties
private byte[] senderPublicKey;
private int version;
// Not always present
private String recipient;
private Long assetId;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long amount;
// Not present if amount is zero
private Long assetId;
private byte[] data;
private boolean isText;
private boolean isEncrypted;
// Constructors
@ -38,19 +47,14 @@ public class MessageTransactionData extends TransactionData {
}
public MessageTransactionData(BaseTransactionData baseTransactionData,
int version, String recipient, Long assetId, long amount, byte[] data, boolean isText, boolean isEncrypted) {
int version, String recipient, long amount, Long assetId, byte[] data, boolean isText, boolean isEncrypted) {
super(TransactionType.MESSAGE, baseTransactionData);
this.senderPublicKey = baseTransactionData.creatorPublicKey;
this.version = version;
this.recipient = recipient;
if (assetId != null)
this.assetId = assetId;
else
this.assetId = Asset.QORT;
this.amount = amount;
this.assetId = assetId;
this.data = data;
this.isText = isText;
this.isEncrypted = isEncrypted;
@ -70,23 +74,23 @@ public class MessageTransactionData extends TransactionData {
return this.recipient;
}
public Long getAssetId() {
return this.assetId;
}
public long getAmount() {
return this.amount;
}
public Long getAssetId() {
return this.assetId;
}
public byte[] getData() {
return this.data;
}
public boolean getIsText() {
public boolean isText() {
return this.isText;
}
public boolean getIsEncrypted() {
public boolean isEncrypted() {
return this.isEncrypted;
}

View File

@ -269,8 +269,8 @@ public class HSQLDBDatabaseUpdates {
case 6:
// Message Transactions
stmt.execute("CREATE TABLE MessageTransactions (signature Signature, version TINYINT NOT NULL, "
+ "sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, "
+ "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, data MessageData NOT NULL, "
+ "sender QortalPublicKey NOT NULL, recipient QortalAddress, amount QortalAmount NOT NULL, asset_id AssetID, "
+ "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, data MessageData NOT NULL, "
+ TRANSACTION_KEYS + ")");
break;

View File

@ -36,7 +36,7 @@ public class HSQLDBMessageTransactionRepository extends HSQLDBTransactionReposit
byte[] data = resultSet.getBytes(7);
return new MessageTransactionData(baseTransactionData, version, recipient, assetId, amount, data, isText, isEncrypted);
return new MessageTransactionData(baseTransactionData, version, recipient, amount, assetId, data, isText, isEncrypted);
} catch (SQLException e) {
throw new DataException("Unable to fetch message transaction from repository", e);
}
@ -50,7 +50,7 @@ public class HSQLDBMessageTransactionRepository extends HSQLDBTransactionReposit
saveHelper.bind("signature", messageTransactionData.getSignature()).bind("version", messageTransactionData.getVersion())
.bind("sender", messageTransactionData.getSenderPublicKey()).bind("recipient", messageTransactionData.getRecipient())
.bind("is_text", messageTransactionData.getIsText()).bind("is_encrypted", messageTransactionData.getIsEncrypted())
.bind("is_text", messageTransactionData.isText()).bind("is_encrypted", messageTransactionData.isEncrypted())
.bind("amount", messageTransactionData.getAmount()).bind("asset_id", messageTransactionData.getAssetId())
.bind("data", messageTransactionData.getData());

View File

@ -4,6 +4,7 @@ import java.util.Collections;
import java.util.List;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.data.PaymentData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
@ -13,14 +14,17 @@ import org.qortal.repository.Repository;
public class MessageTransaction extends Transaction {
// Useful constants
public static final int MAX_DATA_SIZE = 4000;
// Properties
private MessageTransactionData messageTransactionData;
/** Cached, lazy-instantiated payment data. Use {@link #getPaymentData()} instead! */
private PaymentData paymentData = null;
// Other useful constants
public static final int MAX_DATA_SIZE = 4000;
private static final boolean isZeroAmountValid = true;
// Constructors
@ -34,6 +38,9 @@ public class MessageTransaction extends Transaction {
@Override
public List<String> getRecipientAddresses() throws DataException {
if (this.messageTransactionData.getRecipient() == null)
return Collections.emptyList();
return Collections.singletonList(this.messageTransactionData.getRecipient());
}
@ -62,42 +69,80 @@ public class MessageTransaction extends Transaction {
if (this.messageTransactionData.getData().length < 1 || this.messageTransactionData.getData().length > MAX_DATA_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
// If message has no recipient then it cannot have a payment
if (this.messageTransactionData.getRecipient() == null && this.messageTransactionData.getAmount() != 0)
return ValidationResult.INVALID_AMOUNT;
// If message has no payment then we only need to do a simple balance check for fee
if (this.messageTransactionData.getAmount() == 0) {
if (getSender().getConfirmedBalance(Asset.QORT) < this.messageTransactionData.getFee())
return ValidationResult.NO_BALANCE;
return ValidationResult.OK;
}
// Wrap and delegate final payment checks to Payment class
return new Payment(this.repository).isValid(this.messageTransactionData.getSenderPublicKey(), getPaymentData(), this.messageTransactionData.getFee(),
isZeroAmountValid);
return new Payment(this.repository).isValid(this.messageTransactionData.getSenderPublicKey(), getPaymentData(),
this.messageTransactionData.getFee(), true);
}
@Override
public ValidationResult isProcessable() throws DataException {
// If we have no amount then we can always process
if (this.messageTransactionData.getAmount() == 0L)
return ValidationResult.OK;
// Wrap and delegate final processable checks to Payment class
return new Payment(this.repository).isProcessable(this.messageTransactionData.getSenderPublicKey(), getPaymentData(), this.messageTransactionData.getFee(),
isZeroAmountValid);
return new Payment(this.repository).isProcessable(this.messageTransactionData.getSenderPublicKey(),
getPaymentData(), this.messageTransactionData.getFee(), true);
}
@Override
public void process() throws DataException {
// If we have no amount then there's nothing to do
if (this.messageTransactionData.getAmount() == 0L)
return;
// Wrap and delegate payment processing to Payment class.
new Payment(this.repository).process(this.messageTransactionData.getSenderPublicKey(), getPaymentData());
}
@Override
public void processReferencesAndFees() throws DataException {
// If we have no amount then we only need to process sender's reference and fees
if (this.messageTransactionData.getAmount() == 0L) {
super.processReferencesAndFees();
return;
}
// Wrap and delegate references processing to Payment class. Only update recipient's last reference if transferring QORT.
new Payment(this.repository).processReferencesAndFees(this.messageTransactionData.getSenderPublicKey(), getPaymentData(), this.messageTransactionData.getFee(),
this.messageTransactionData.getSignature(), false);
new Payment(this.repository).processReferencesAndFees(this.messageTransactionData.getSenderPublicKey(),
getPaymentData(), this.messageTransactionData.getFee(), this.messageTransactionData.getSignature(),
false);
}
@Override
public void orphan() throws DataException {
// If we have no amount then there's nothing to do
if (this.messageTransactionData.getAmount() == 0L)
return;
// Wrap and delegate payment processing to Payment class.
new Payment(this.repository).orphan(this.messageTransactionData.getSenderPublicKey(), getPaymentData());
}
@Override
public void orphanReferencesAndFees() throws DataException {
// If we have no amount then we only need to orphan sender's reference and fees
if (this.messageTransactionData.getAmount() == 0L) {
super.orphanReferencesAndFees();
return;
}
// Wrap and delegate references processing to Payment class. Only revert recipient's last reference if transferring QORT.
new Payment(this.repository).orphanReferencesAndFees(this.messageTransactionData.getSenderPublicKey(), getPaymentData(), this.messageTransactionData.getFee(),
this.messageTransactionData.getSignature(), this.messageTransactionData.getReference(), false);
new Payment(this.repository).orphanReferencesAndFees(this.messageTransactionData.getSenderPublicKey(),
getPaymentData(), this.messageTransactionData.getFee(), this.messageTransactionData.getSignature(),
this.messageTransactionData.getReference(), false);
}
}

View File

@ -19,12 +19,13 @@ import com.google.common.primitives.Longs;
public class MessageTransactionTransformer extends TransactionTransformer {
// Property lengths
private static final int HAS_RECIPIENT_LENGTH = BOOLEAN_LENGTH;
private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH;
private static final int DATA_SIZE_LENGTH = INT_LENGTH;
private static final int IS_TEXT_LENGTH = BOOLEAN_LENGTH;
private static final int IS_ENCRYPTED_LENGTH = BOOLEAN_LENGTH;
private static final int EXTRAS_LENGTH = RECIPIENT_LENGTH + ASSET_ID_LENGTH + AMOUNT_LENGTH + DATA_SIZE_LENGTH + IS_ENCRYPTED_LENGTH + IS_TEXT_LENGTH;
private static final int EXTRAS_LENGTH = HAS_RECIPIENT_LENGTH + AMOUNT_LENGTH + DATA_SIZE_LENGTH + IS_ENCRYPTED_LENGTH + IS_TEXT_LENGTH;
protected static final TransactionLayout layout;
@ -35,9 +36,10 @@ public class MessageTransactionTransformer extends TransactionTransformer {
layout.add("transaction's groupID", TransformationType.INT);
layout.add("reference", TransformationType.SIGNATURE);
layout.add("sender's public key", TransformationType.PUBLIC_KEY);
layout.add("recipient", TransformationType.ADDRESS);
layout.add("asset ID of payment", TransformationType.LONG);
layout.add("has recipient?", TransformationType.BOOLEAN);
layout.add("? recipient", TransformationType.ADDRESS);
layout.add("payment (can be zero)", TransformationType.AMOUNT);
layout.add("asset ID of payment (if payment not zero)", TransformationType.LONG);
layout.add("message length", TransformationType.INT);
layout.add("message", TransformationType.DATA);
layout.add("is message encrypted?", TransformationType.BOOLEAN);
@ -58,12 +60,13 @@ public class MessageTransactionTransformer extends TransactionTransformer {
byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer);
String recipient = Serialization.deserializeAddress(byteBuffer);
long assetId = byteBuffer.getLong();
boolean hasRecipient = byteBuffer.get() != 0;
String recipient = hasRecipient ? Serialization.deserializeAddress(byteBuffer) : null;
long amount = byteBuffer.getLong();
Long assetId = amount != 0 ? byteBuffer.getLong() : null;
int dataSize = byteBuffer.getInt();
// Don't allow invalid dataSize here to avoid run-time issues
if (dataSize > MessageTransaction.MAX_DATA_SIZE)
@ -83,13 +86,21 @@ public class MessageTransactionTransformer extends TransactionTransformer {
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, senderPublicKey, fee, signature);
return new MessageTransactionData(baseTransactionData, version, recipient, assetId, amount, data, isText, isEncrypted);
return new MessageTransactionData(baseTransactionData, version, recipient, amount, assetId, data, isText, isEncrypted);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData;
return getBaseLength(transactionData) + EXTRAS_LENGTH + messageTransactionData.getData().length;
int dataLength = getBaseLength(transactionData) + EXTRAS_LENGTH + messageTransactionData.getData().length;
if (messageTransactionData.getRecipient() != null)
dataLength += RECIPIENT_LENGTH;
if (messageTransactionData.getAmount() != 0)
dataLength += ASSET_ID_LENGTH;
return dataLength;
}
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
@ -100,19 +111,25 @@ public class MessageTransactionTransformer extends TransactionTransformer {
transformCommonBytes(transactionData, bytes);
Serialization.serializeAddress(bytes, messageTransactionData.getRecipient());
bytes.write(Longs.toByteArray(messageTransactionData.getAssetId()));
if (messageTransactionData.getRecipient() != null) {
bytes.write((byte) 1);
Serialization.serializeAddress(bytes, messageTransactionData.getRecipient());
} else {
bytes.write((byte) 0);
}
bytes.write(Longs.toByteArray(messageTransactionData.getAmount()));
if (messageTransactionData.getAmount() != 0)
bytes.write(Longs.toByteArray(messageTransactionData.getAssetId()));
bytes.write(Ints.toByteArray(messageTransactionData.getData().length));
bytes.write(messageTransactionData.getData());
bytes.write((byte) (messageTransactionData.getIsEncrypted() ? 1 : 0));
bytes.write((byte) (messageTransactionData.isEncrypted() ? 1 : 0));
bytes.write((byte) (messageTransactionData.getIsText() ? 1 : 0));
bytes.write((byte) (messageTransactionData.isText() ? 1 : 0));
bytes.write(Longs.toByteArray(messageTransactionData.getFee()));

View File

@ -0,0 +1,154 @@
package org.qortal.test;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.asset.Asset;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.group.Group.ApprovalThreshold;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.GroupUtils;
import org.qortal.test.common.TestAccount;
import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.MessageTransactionTransformer;
import org.qortal.transform.transaction.TransactionTransformer;
import static org.junit.Assert.*;
public class MessageTests extends Common {
private static final int version = 3;
private static final String recipient = Common.getTestAccount(null, "bob").getAddress();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@After
public void afterTest() throws DataException {
Common.orphanCheck();
}
@Test
public void validityTests() throws DataException {
// with recipient, with amount
assertTrue(isValid(Group.NO_GROUP, recipient, 123L, Asset.QORT));
// with recipient, no amount
assertTrue(isValid(Group.NO_GROUP, recipient, 0L, null));
// no recipient (message to group), no amount
assertTrue(isValid(Group.NO_GROUP, null, 0L, null));
// can't have amount if no recipient!
assertFalse(isValid(Group.NO_GROUP, null, 123L, Asset.QORT));
// Alice is part of group 1
assertTrue(isValid(1, null, 0L, null));
int newGroupId;
try (final Repository repository = RepositoryManager.getRepository()) {
newGroupId = GroupUtils.createGroup(repository, "chloe", "non-alice-group", false, ApprovalThreshold.ONE, 10, 1440);
}
// Alice is not part of new group
assertFalse(isValid(newGroupId, null, 0L, null));
}
@Test
public void withRecipentNoAmount() throws DataException {
testMessage(Group.NO_GROUP, recipient, 0L, null);
}
@Test
public void withRecipentWithAmount() throws DataException {
testMessage(Group.NO_GROUP, recipient, 123L, Asset.QORT);
}
@Test
public void noRecipentNoAmount() throws DataException {
testMessage(Group.NO_GROUP, null, 0L, null);
}
@Test
public void noRecipentNoAmountWithGroup() throws DataException {
testMessage(1, null, 0L, null);
}
@Test
public void serializationTests() throws DataException, TransformationException {
// with recipient, with amount
testSerialization(recipient, 123L, Asset.QORT);
// with recipient, no amount
testSerialization(recipient, 0L, null);
// no recipient (message to group), no amount
testSerialization(null, 0L, null);
}
private boolean isValid(int txGroupId, String recipient, long amount, Long assetId) throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId),
version, recipient, amount, assetId, new byte[1], false, false);
Transaction transaction = new MessageTransaction(repository, transactionData);
return transaction.isValidUnconfirmed() == ValidationResult.OK;
}
}
private void testMessage(int txGroupId, String recipient, long amount, Long assetId) throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId),
version, recipient, amount, assetId, new byte[1], false, false);
TransactionUtils.signAndMint(repository, transactionData, alice);
BlockUtils.orphanLastBlock(repository);
}
}
private void testSerialization(String recipient, long amount, Long assetId) throws DataException, TransformationException {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
MessageTransactionData expectedTransactionData = new MessageTransactionData(TestTransaction.generateBase(alice),
version, recipient, amount, assetId, new byte[1], false, false);
Transaction transaction = new MessageTransaction(repository, expectedTransactionData);
transaction.sign(alice);
MessageTransactionTransformer.getDataLength(expectedTransactionData);
byte[] transactionBytes = MessageTransactionTransformer.toBytes(expectedTransactionData);
TransactionData transactionData = TransactionTransformer.fromBytes(transactionBytes);
assertEquals(TransactionType.MESSAGE, transactionData.getType());
MessageTransactionData actualTransactionData = (MessageTransactionData) transactionData;
assertEquals(expectedTransactionData.getRecipient(), actualTransactionData.getRecipient());
assertEquals(expectedTransactionData.getAmount(), actualTransactionData.getAmount());
assertEquals(expectedTransactionData.getAssetId(), actualTransactionData.getAssetId());
}
}
}

View File

@ -474,9 +474,10 @@ public class AtTests extends Common {
Long fee = null;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, 4, recipient, Asset.QORT, amount, data, false, false);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, 4, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);

View File

@ -19,7 +19,7 @@ public class MessageTestTransaction extends TestTransaction {
final boolean isText = true;
final boolean isEncrypted = false;
return new MessageTransactionData(generateBase(account), version, recipient, assetId, amount, data, isText, isEncrypted);
return new MessageTransactionData(generateBase(account), version, recipient, amount, assetId, data, isText, isEncrypted);
}
}

View File

@ -12,8 +12,12 @@ public abstract class TestTransaction {
protected static final Random random = new Random();
public static BaseTransactionData generateBase(PrivateKeyAccount account, int txGroupId) throws DataException {
return new BaseTransactionData(System.currentTimeMillis(), txGroupId, account.getLastReference(), account.getPublicKey(), BlockChain.getInstance().getUnitFee(), null);
}
public static BaseTransactionData generateBase(PrivateKeyAccount account) throws DataException {
return new BaseTransactionData(System.currentTimeMillis(), Group.NO_GROUP, account.getLastReference(), account.getPublicKey(), BlockChain.getInstance().getUnitFee(), null);
return generateBase(account, Group.NO_GROUP);
}
}