diff --git a/src/data/transaction/CancelSellNameTransactionData.java b/src/data/transaction/CancelSellNameTransactionData.java new file mode 100644 index 00000000..17c62bb5 --- /dev/null +++ b/src/data/transaction/CancelSellNameTransactionData.java @@ -0,0 +1,36 @@ +package data.transaction; + +import java.math.BigDecimal; + +import qora.transaction.Transaction.TransactionType; + +public class CancelSellNameTransactionData extends TransactionData { + + // Properties + private byte[] ownerPublicKey; + private String name; + + // Constructors + + public CancelSellNameTransactionData(byte[] ownerPublicKey, String name, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + super(TransactionType.CANCEL_SELL_NAME, fee, ownerPublicKey, timestamp, reference, signature); + + this.ownerPublicKey = ownerPublicKey; + this.name = name; + } + + public CancelSellNameTransactionData(byte[] ownerPublicKey, String name, BigDecimal fee, long timestamp, byte[] reference) { + this(ownerPublicKey, name, fee, timestamp, reference, null); + } + + // Getters / setters + + public byte[] getOwnerPublicKey() { + return this.ownerPublicKey; + } + + public String getName() { + return this.name; + } + +} diff --git a/src/qora/naming/Name.java b/src/qora/naming/Name.java index 45ee75ec..ece09e2e 100644 --- a/src/qora/naming/Name.java +++ b/src/qora/naming/Name.java @@ -1,6 +1,7 @@ package qora.naming; import data.naming.NameData; +import data.transaction.CancelSellNameTransactionData; import data.transaction.RegisterNameTransactionData; import data.transaction.SellNameTransactionData; import data.transaction.TransactionData; @@ -118,4 +119,20 @@ public class Name { this.repository.getNameRepository().save(this.nameData); } + public void sell(CancelSellNameTransactionData cancelSellNameTransactionData) throws DataException { + // Mark not for-sale but leave price in case we want to orphan + this.nameData.setIsForSale(false); + + // Save sale info into repository + this.repository.getNameRepository().save(this.nameData); + } + + public void unsell(CancelSellNameTransactionData cancelSellNameTransactionData) throws DataException { + // Mark as for-sale using existing price + this.nameData.setIsForSale(true); + + // Save no-sale info into repository + this.repository.getNameRepository().save(this.nameData); + } + } diff --git a/src/qora/transaction/CancelSellNameTransaction.java b/src/qora/transaction/CancelSellNameTransaction.java new file mode 100644 index 00000000..b2e36572 --- /dev/null +++ b/src/qora/transaction/CancelSellNameTransaction.java @@ -0,0 +1,145 @@ +package qora.transaction; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.google.common.base.Utf8; + +import data.naming.NameData; +import data.transaction.CancelSellNameTransactionData; +import data.transaction.TransactionData; +import qora.account.Account; +import qora.account.PublicKeyAccount; +import qora.assets.Asset; +import qora.naming.Name; +import repository.DataException; +import repository.Repository; + +public class CancelSellNameTransaction extends Transaction { + + // Properties + private CancelSellNameTransactionData cancelSellNameTransactionData; + + // Constructors + + public CancelSellNameTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + + this.cancelSellNameTransactionData = (CancelSellNameTransactionData) this.transactionData; + } + + // More information + + @Override + public List getRecipientAccounts() { + return new ArrayList(); + } + + @Override + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getOwner().getAddress())) + return true; + + return false; + } + + @Override + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + + if (address.equals(this.getOwner().getAddress())) + amount = amount.subtract(this.transactionData.getFee()); + + return amount; + } + + // Navigation + + public Account getOwner() throws DataException { + return new PublicKeyAccount(this.repository, this.cancelSellNameTransactionData.getOwnerPublicKey()); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + // Check name size bounds + int nameLength = Utf8.encodedLength(cancelSellNameTransactionData.getName()); + if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE) + return ValidationResult.INVALID_NAME_LENGTH; + + // Check name is lowercase + if (!cancelSellNameTransactionData.getName().equals(cancelSellNameTransactionData.getName().toLowerCase())) + return ValidationResult.NAME_NOT_LOWER_CASE; + + NameData nameData = this.repository.getNameRepository().fromName(cancelSellNameTransactionData.getName()); + + // Check name exists + if (nameData == null) + return ValidationResult.NAME_DOES_NOT_EXIST; + + // Check name is currently for sale + if (!nameData.getIsForSale()) + return ValidationResult.NAME_NOT_FOR_SALE; + + // Check transaction's public key matches name's current owner + Account owner = new PublicKeyAccount(this.repository, cancelSellNameTransactionData.getOwnerPublicKey()); + if (!owner.getAddress().equals(nameData.getOwner())) + return ValidationResult.INVALID_NAME_OWNER; + + // Check fee is positive + if (cancelSellNameTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + // Check reference is correct + if (!Arrays.equals(owner.getLastReference(), cancelSellNameTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + // Check issuer has enough funds + if (owner.getConfirmedBalance(Asset.QORA).compareTo(cancelSellNameTransactionData.getFee()) == -1) + return ValidationResult.NO_BALANCE; + + return ValidationResult.OK; + + } + + @Override + public void process() throws DataException { + // Update Name + Name name = new Name(this.repository, cancelSellNameTransactionData.getName()); + name.sell(cancelSellNameTransactionData); + + // Save this transaction, now with updated "name reference" to previous transaction that updated name + this.repository.getTransactionRepository().save(cancelSellNameTransactionData); + + // Update owner's balance + Account owner = new PublicKeyAccount(this.repository, cancelSellNameTransactionData.getOwnerPublicKey()); + owner.setConfirmedBalance(Asset.QORA, owner.getConfirmedBalance(Asset.QORA).subtract(cancelSellNameTransactionData.getFee())); + + // Update owner's reference + owner.setLastReference(cancelSellNameTransactionData.getSignature()); + } + + @Override + public void orphan() throws DataException { + // Revert name + Name name = new Name(this.repository, cancelSellNameTransactionData.getName()); + name.unsell(cancelSellNameTransactionData); + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(cancelSellNameTransactionData); + + // Update owner's balance + Account owner = new PublicKeyAccount(this.repository, cancelSellNameTransactionData.getOwnerPublicKey()); + owner.setConfirmedBalance(Asset.QORA, owner.getConfirmedBalance(Asset.QORA).add(cancelSellNameTransactionData.getFee())); + + // Update owner's reference + owner.setLastReference(cancelSellNameTransactionData.getReference()); + } + +} diff --git a/src/qora/transaction/CreatePollTransaction.java b/src/qora/transaction/CreatePollTransaction.java index eaea8ac8..7a64875d 100644 --- a/src/qora/transaction/CreatePollTransaction.java +++ b/src/qora/transaction/CreatePollTransaction.java @@ -1,12 +1,13 @@ package qora.transaction; import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import com.google.common.base.Utf8; + import data.transaction.CreatePollTransactionData; import data.transaction.TransactionData; import data.voting.PollOptionData; @@ -84,12 +85,13 @@ public class CreatePollTransaction extends Transaction { return ValidationResult.INVALID_ADDRESS; // Check name size bounds - if (createPollTransactionData.getPollName().length() < 1 || createPollTransactionData.getPollName().length() > Poll.MAX_NAME_SIZE) + int pollNameLength = Utf8.encodedLength(createPollTransactionData.getPollName()); + if (pollNameLength < 1 || pollNameLength > Poll.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; // Check description size bounds - if (createPollTransactionData.getDescription().length() < 1 - || createPollTransactionData.getDescription().length() > Poll.MAX_DESCRIPTION_SIZE) + int pollDescriptionLength = Utf8.encodedLength(createPollTransactionData.getDescription()); + if (pollDescriptionLength < 1 || pollDescriptionLength > Poll.MAX_DESCRIPTION_SIZE) return ValidationResult.INVALID_DESCRIPTION_LENGTH; // Check poll name is lowercase @@ -112,7 +114,7 @@ public class CreatePollTransaction extends Transaction { List optionNames = new ArrayList(); for (PollOptionData pollOptionData : pollOptions) { // Check option length - int optionNameLength = pollOptionData.getOptionName().getBytes(StandardCharsets.UTF_8).length; + int optionNameLength = Utf8.encodedLength(pollOptionData.getOptionName()); if (optionNameLength < 1 || optionNameLength > Poll.MAX_NAME_SIZE) return ValidationResult.INVALID_OPTION_LENGTH; diff --git a/src/qora/transaction/IssueAssetTransaction.java b/src/qora/transaction/IssueAssetTransaction.java index 080fe75b..fde4e712 100644 --- a/src/qora/transaction/IssueAssetTransaction.java +++ b/src/qora/transaction/IssueAssetTransaction.java @@ -5,6 +5,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import com.google.common.base.Utf8; + import data.transaction.IssueAssetTransactionData; import data.transaction.TransactionData; import qora.account.Account; @@ -85,12 +87,13 @@ public class IssueAssetTransaction extends Transaction { return ValidationResult.INVALID_ADDRESS; // Check name size bounds - if (issueAssetTransactionData.getAssetName().length() < 1 || issueAssetTransactionData.getAssetName().length() > IssueAssetTransaction.MAX_NAME_SIZE) + int assetNameLength = Utf8.encodedLength(issueAssetTransactionData.getAssetName()); + if (assetNameLength < 1 || assetNameLength > IssueAssetTransaction.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; // Check description size bounds - if (issueAssetTransactionData.getDescription().length() < 1 - || issueAssetTransactionData.getDescription().length() > IssueAssetTransaction.MAX_DESCRIPTION_SIZE) + int assetDescriptionlength = Utf8.encodedLength(issueAssetTransactionData.getDescription()); + if (assetDescriptionlength < 1 || assetDescriptionlength > IssueAssetTransaction.MAX_DESCRIPTION_SIZE) return ValidationResult.INVALID_DESCRIPTION_LENGTH; // Check quantity - either 10 billion or if that's not enough: a billion billion! diff --git a/src/qora/transaction/RegisterNameTransaction.java b/src/qora/transaction/RegisterNameTransaction.java index bd6affeb..6051de31 100644 --- a/src/qora/transaction/RegisterNameTransaction.java +++ b/src/qora/transaction/RegisterNameTransaction.java @@ -5,6 +5,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import com.google.common.base.Utf8; + import data.transaction.RegisterNameTransactionData; import data.transaction.TransactionData; import qora.account.Account; @@ -78,11 +80,13 @@ public class RegisterNameTransaction extends Transaction { return ValidationResult.INVALID_ADDRESS; // Check name size bounds - if (registerNameTransactionData.getName().length() < 1 || registerNameTransactionData.getName().length() > Name.MAX_NAME_SIZE) + int nameLength = Utf8.encodedLength(registerNameTransactionData.getName()); + if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; - // Check value size bounds - if (registerNameTransactionData.getData().length() < 1 || registerNameTransactionData.getData().length() > Name.MAX_DATA_SIZE) + // Check data size bounds + int dataLength = Utf8.encodedLength(registerNameTransactionData.getData()); + if (dataLength < 1 || dataLength > Name.MAX_DATA_SIZE) return ValidationResult.INVALID_DATA_LENGTH; // Check name is lowercase diff --git a/src/qora/transaction/SellNameTransaction.java b/src/qora/transaction/SellNameTransaction.java index 2498f9bd..a718efee 100644 --- a/src/qora/transaction/SellNameTransaction.java +++ b/src/qora/transaction/SellNameTransaction.java @@ -5,6 +5,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import com.google.common.base.Utf8; + import data.naming.NameData; import data.transaction.SellNameTransactionData; import data.transaction.TransactionData; @@ -68,7 +70,8 @@ public class SellNameTransaction extends Transaction { @Override public ValidationResult isValid() throws DataException { // Check name size bounds - if (sellNameTransactionData.getName().length() < 1 || sellNameTransactionData.getName().length() > Name.MAX_NAME_SIZE) + int nameLength = Utf8.encodedLength(sellNameTransactionData.getName()); + if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; // Check name is lowercase diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index 9fe0b541..9df0e5f5 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -44,7 +44,7 @@ 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_VALUE_LENGTH( - 8), NAME_ALREADY_REGISTERED(9), NAME_DOES_NOT_EXIST(10), INVALID_NAME_OWNER(11), NAME_ALREADY_FOR_SALE(12), INVALID_AMOUNT( + 8), NAME_ALREADY_REGISTERED(9), NAME_DOES_NOT_EXIST(10), INVALID_NAME_OWNER(11), NAME_ALREADY_FOR_SALE(12), NAME_NOT_FOR_SALE(13), INVALID_AMOUNT( 15), NAME_NOT_LOWER_CASE(17), INVALID_DESCRIPTION_LENGTH(18), INVALID_OPTIONS_COUNT(19), INVALID_OPTION_LENGTH(20), DUPLICATE_OPTION( 21), POLL_ALREADY_EXISTS(22), POLL_DOES_NOT_EXIST(24), POLL_OPTION_DOES_NOT_EXIST(25), ALREADY_VOTED_FOR_THAT_OPTION( 26), INVALID_DATA_LENGTH(27), INVALID_QUANTITY(28), ASSET_DOES_NOT_EXIST(29), INVALID_RETURN(30), HAVE_EQUALS_WANT( @@ -114,6 +114,9 @@ public abstract class Transaction { case SELL_NAME: return new SellNameTransaction(repository, transactionData); + case CANCEL_SELL_NAME: + return new CancelSellNameTransaction(repository, transactionData); + case CREATE_POLL: return new CreatePollTransaction(repository, transactionData); diff --git a/src/qora/transaction/UpdateNameTransaction.java b/src/qora/transaction/UpdateNameTransaction.java index 4492769f..6e3cd3c6 100644 --- a/src/qora/transaction/UpdateNameTransaction.java +++ b/src/qora/transaction/UpdateNameTransaction.java @@ -5,6 +5,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import com.google.common.base.Utf8; + import data.transaction.UpdateNameTransactionData; import data.naming.NameData; import data.transaction.TransactionData; @@ -79,11 +81,13 @@ public class UpdateNameTransaction extends Transaction { return ValidationResult.INVALID_ADDRESS; // Check name size bounds - if (updateNameTransactionData.getName().length() < 1 || updateNameTransactionData.getName().length() > Name.MAX_NAME_SIZE) + int nameLength = Utf8.encodedLength(updateNameTransactionData.getName()); + if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; - // Check value size bounds - if (updateNameTransactionData.getNewData().length() < 1 || updateNameTransactionData.getNewData().length() > Name.MAX_DATA_SIZE) + // Check new data size bounds + int newDataLength = Utf8.encodedLength(updateNameTransactionData.getNewData()); + if (newDataLength < 1 || newDataLength > Name.MAX_DATA_SIZE) return ValidationResult.INVALID_DATA_LENGTH; // Check name is lowercase diff --git a/src/qora/transaction/VoteOnPollTransaction.java b/src/qora/transaction/VoteOnPollTransaction.java index 7f1f6ce3..794daef9 100644 --- a/src/qora/transaction/VoteOnPollTransaction.java +++ b/src/qora/transaction/VoteOnPollTransaction.java @@ -5,6 +5,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import com.google.common.base.Utf8; + import data.transaction.TransactionData; import data.transaction.VoteOnPollTransactionData; import data.voting.PollData; @@ -64,7 +66,8 @@ public class VoteOnPollTransaction extends Transaction { return ValidationResult.NOT_YET_RELEASED; // Check name size bounds - if (voteOnPollTransactionData.getPollName().length() < 1 || voteOnPollTransactionData.getPollName().length() > Poll.MAX_NAME_SIZE) + int pollNameLength = Utf8.encodedLength(voteOnPollTransactionData.getPollName()); + if (pollNameLength < 1 || pollNameLength > Poll.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; // Check poll name is lowercase diff --git a/src/repository/hsqldb/transaction/HSQLDBCancelSellNameTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBCancelSellNameTransactionRepository.java new file mode 100644 index 00000000..e8d09223 --- /dev/null +++ b/src/repository/hsqldb/transaction/HSQLDBCancelSellNameTransactionRepository.java @@ -0,0 +1,49 @@ +package repository.hsqldb.transaction; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import data.transaction.CancelSellNameTransactionData; +import data.transaction.TransactionData; +import repository.DataException; +import repository.hsqldb.HSQLDBRepository; +import repository.hsqldb.HSQLDBSaver; + +public class HSQLDBCancelSellNameTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBCancelSellNameTransactionRepository(HSQLDBRepository repository) { + this.repository = repository; + } + + TransactionData fromBase(byte[] signature, byte[] reference, byte[] ownerPublicKey, long timestamp, BigDecimal fee) throws DataException { + try { + ResultSet rs = this.repository.checkedExecute("SELECT name FROM CancelSellNameTransactions WHERE signature = ?", signature); + if (rs == null) + return null; + + String name = rs.getString(1); + + return new CancelSellNameTransactionData(ownerPublicKey, name, fee, timestamp, reference, signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch cancel sell name transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("CancelSellNameTransactions"); + + saveHelper.bind("signature", cancelSellNameTransactionData.getSignature()).bind("owner", cancelSellNameTransactionData.getOwnerPublicKey()).bind("name", + cancelSellNameTransactionData.getName()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save cancel sell name transaction into repository", e); + } + } + +} diff --git a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index 92dcca7b..41acff47 100644 --- a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -24,6 +24,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { private HSQLDBRegisterNameTransactionRepository registerNameTransactionRepository; private HSQLDBUpdateNameTransactionRepository updateNameTransactionRepository; private HSQLDBSellNameTransactionRepository sellNameTransactionRepository; + private HSQLDBCancelSellNameTransactionRepository cancelSellNameTransactionRepository; private HSQLDBCreatePollTransactionRepository createPollTransactionRepository; private HSQLDBVoteOnPollTransactionRepository voteOnPollTransactionRepository; private HSQLDBIssueAssetTransactionRepository issueAssetTransactionRepository; @@ -40,6 +41,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.registerNameTransactionRepository = new HSQLDBRegisterNameTransactionRepository(repository); this.updateNameTransactionRepository = new HSQLDBUpdateNameTransactionRepository(repository); this.sellNameTransactionRepository = new HSQLDBSellNameTransactionRepository(repository); + this.cancelSellNameTransactionRepository = new HSQLDBCancelSellNameTransactionRepository(repository); this.createPollTransactionRepository = new HSQLDBCreatePollTransactionRepository(repository); this.voteOnPollTransactionRepository = new HSQLDBVoteOnPollTransactionRepository(repository); this.issueAssetTransactionRepository = new HSQLDBIssueAssetTransactionRepository(repository); @@ -107,6 +109,9 @@ public class HSQLDBTransactionRepository implements TransactionRepository { case SELL_NAME: return this.sellNameTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case CANCEL_SELL_NAME: + return this.cancelSellNameTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case CREATE_POLL: return this.createPollTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); @@ -247,6 +252,10 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.sellNameTransactionRepository.save(transactionData); break; + case CANCEL_SELL_NAME: + this.cancelSellNameTransactionRepository.save(transactionData); + break; + case CREATE_POLL: this.createPollTransactionRepository.save(transactionData); break; diff --git a/src/test/SerializationTests.java b/src/test/SerializationTests.java index 0fd4080c..980a4af6 100644 --- a/src/test/SerializationTests.java +++ b/src/test/SerializationTests.java @@ -101,7 +101,12 @@ public class SerializationTests extends Common { @Test public void testSellNameSerialization() throws TransformationException, DataException { - testSpecificBlockTransactions(673, TransactionType.SELL_NAME); + testSpecificBlockTransactions(200, TransactionType.SELL_NAME); + } + + @Test + public void testCancelSellNameSerialization() throws TransformationException, DataException { + testSpecificBlockTransactions(741, TransactionType.CANCEL_SELL_NAME); } @Test diff --git a/src/test/TransactionTests.java b/src/test/TransactionTests.java index b22679ac..c3348d25 100644 --- a/src/test/TransactionTests.java +++ b/src/test/TransactionTests.java @@ -16,6 +16,7 @@ import data.account.AccountBalanceData; import data.account.AccountData; import data.block.BlockData; import data.naming.NameData; +import data.transaction.CancelSellNameTransactionData; import data.transaction.CreatePollTransactionData; import data.transaction.PaymentTransactionData; import data.transaction.RegisterNameTransactionData; @@ -31,6 +32,7 @@ import qora.account.PublicKeyAccount; import qora.assets.Asset; import qora.block.Block; import qora.block.BlockChain; +import qora.transaction.CancelSellNameTransaction; import qora.transaction.CreatePollTransaction; import qora.transaction.PaymentTransaction; import qora.transaction.RegisterNameTransaction; @@ -223,7 +225,7 @@ public class TransactionTests { byte[] nameReference = reference; BigDecimal fee = BigDecimal.ONE; - long timestamp = parentBlockData.getTimestamp() + 2_000; + long timestamp = parentBlockData.getTimestamp() + 1_000; UpdateNameTransactionData updateNameTransactionData = new UpdateNameTransactionData(sender.getPublicKey(), newOwner.getAddress(), name, newData, nameReference, fee, timestamp, reference); @@ -269,7 +271,7 @@ public class TransactionTests { BigDecimal amount = BigDecimal.valueOf(1234L).setScale(8); BigDecimal fee = BigDecimal.ONE; - long timestamp = parentBlockData.getTimestamp() + 2_000; + long timestamp = parentBlockData.getTimestamp() + 1_000; SellNameTransactionData sellNameTransactionData = new SellNameTransactionData(sender.getPublicKey(), name, amount, fee, timestamp, reference); Transaction sellNameTransaction = new SellNameTransaction(repository, sellNameTransactionData); @@ -301,6 +303,61 @@ public class TransactionTests { actualNameData = this.repository.getNameRepository().fromName(name); assertFalse(actualNameData.getIsForSale()); assertNull(actualNameData.getSalePrice()); + + // Re-process block for use by other tests + block.process(); + repository.saveChanges(); + + // Update variables for use by other tests + reference = sender.getLastReference(); + parentBlockData = block.getBlockData(); + } + + @Test + public void testCancelSellNameTransaction() throws DataException { + // Register and sell name using another test + testSellNameTransaction(); + + String name = "test name"; + NameData originalNameData = this.repository.getNameRepository().fromName(name); + + BigDecimal fee = BigDecimal.ONE; + long timestamp = parentBlockData.getTimestamp() + 1_000; + CancelSellNameTransactionData cancelSellNameTransactionData = new CancelSellNameTransactionData(sender.getPublicKey(), name, fee, timestamp, reference); + + Transaction cancelSellNameTransaction = new CancelSellNameTransaction(repository, cancelSellNameTransactionData); + cancelSellNameTransaction.calcSignature(sender); + assertTrue(cancelSellNameTransaction.isSignatureValid()); + assertEquals(ValidationResult.OK, cancelSellNameTransaction.isValid()); + + // Forge new block with transaction + Block block = new Block(repository, parentBlockData, generator, null, null); + block.addTransaction(cancelSellNameTransactionData); + block.sign(); + + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + + block.process(); + repository.saveChanges(); + + // Check name was updated + NameData actualNameData = this.repository.getNameRepository().fromName(name); + assertFalse(actualNameData.getIsForSale()); + assertEquals(originalNameData.getSalePrice(), actualNameData.getSalePrice()); + + // Now orphan block + block.orphan(); + repository.saveChanges(); + + // Check name has been reverted correctly + actualNameData = this.repository.getNameRepository().fromName(name); + assertTrue(actualNameData.getIsForSale()); + assertEquals(originalNameData.getSalePrice(), actualNameData.getSalePrice()); + + // Update variables for use by other tests + reference = sender.getLastReference(); + parentBlockData = block.getBlockData(); } @Test diff --git a/src/transform/transaction/CancelSellNameTransactionTransformer.java b/src/transform/transaction/CancelSellNameTransactionTransformer.java new file mode 100644 index 00000000..542b9c01 --- /dev/null +++ b/src/transform/transaction/CancelSellNameTransactionTransformer.java @@ -0,0 +1,107 @@ +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.base.Utf8; +import com.google.common.hash.HashCode; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +import data.transaction.CancelSellNameTransactionData; +import data.transaction.TransactionData; +import qora.account.PublicKeyAccount; +import qora.naming.Name; +import transform.TransformationException; +import utils.Serialization; + +public class CancelSellNameTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int OWNER_LENGTH = PUBLIC_KEY_LENGTH; + private static final int NAME_SIZE_LENGTH = INT_LENGTH; + + private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + OWNER_LENGTH + NAME_SIZE_LENGTH; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + if (byteBuffer.remaining() < TYPELESS_DATALESS_LENGTH) + throw new TransformationException("Byte data too short for CancelSellNameTransaction"); + + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] ownerPublicKey = Serialization.deserializePublicKey(byteBuffer); + + String name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE); + + // Still need to make sure there are enough bytes left for remaining fields + if (byteBuffer.remaining() < FEE_LENGTH + SIGNATURE_LENGTH) + throw new TransformationException("Byte data too short for CancelSellNameTransaction"); + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new CancelSellNameTransactionData(ownerPublicKey, name, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) transactionData; + + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(cancelSellNameTransactionData.getName()); + + return dataLength; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(cancelSellNameTransactionData.getType().value)); + bytes.write(Longs.toByteArray(cancelSellNameTransactionData.getTimestamp())); + bytes.write(cancelSellNameTransactionData.getReference()); + + bytes.write(cancelSellNameTransactionData.getOwnerPublicKey()); + Serialization.serializeSizedString(bytes, cancelSellNameTransactionData.getName()); + + Serialization.serializeBigDecimal(bytes, cancelSellNameTransactionData.getFee()); + + if (cancelSellNameTransactionData.getSignature() != null) + bytes.write(cancelSellNameTransactionData.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 { + CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) transactionData; + + byte[] ownerPublicKey = cancelSellNameTransactionData.getOwnerPublicKey(); + + json.put("owner", PublicKeyAccount.getAddress(ownerPublicKey)); + json.put("ownerPublicKey", HashCode.fromBytes(ownerPublicKey).toString()); + + json.put("name", cancelSellNameTransactionData.getName()); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/transform/transaction/CreatePollTransactionTransformer.java b/src/transform/transaction/CreatePollTransactionTransformer.java index f8584aa7..971c0ac4 100644 --- a/src/transform/transaction/CreatePollTransactionTransformer.java +++ b/src/transform/transaction/CreatePollTransactionTransformer.java @@ -10,6 +10,7 @@ import java.util.List; import org.json.simple.JSONArray; import org.json.simple.JSONObject; +import com.google.common.base.Utf8; import com.google.common.hash.HashCode; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; @@ -81,12 +82,12 @@ public class CreatePollTransactionTransformer extends TransactionTransformer { public static int getDataLength(TransactionData transactionData) throws TransformationException { CreatePollTransactionData createPollTransactionData = (CreatePollTransactionData) transactionData; - int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + createPollTransactionData.getPollName().length() - + createPollTransactionData.getDescription().length(); + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(createPollTransactionData.getPollName()) + + Utf8.encodedLength(createPollTransactionData.getDescription()); // Add lengths for each poll options for (PollOptionData pollOptionData : createPollTransactionData.getPollOptions()) - dataLength += INT_LENGTH + pollOptionData.getOptionName().length(); + dataLength += INT_LENGTH + Utf8.encodedLength(pollOptionData.getOptionName()); return dataLength; } diff --git a/src/transform/transaction/IssueAssetTransactionTransformer.java b/src/transform/transaction/IssueAssetTransactionTransformer.java index 7bf1e677..9319a5b2 100644 --- a/src/transform/transaction/IssueAssetTransactionTransformer.java +++ b/src/transform/transaction/IssueAssetTransactionTransformer.java @@ -7,6 +7,7 @@ import java.nio.ByteBuffer; import org.json.simple.JSONObject; +import com.google.common.base.Utf8; import com.google.common.hash.HashCode; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; @@ -65,7 +66,8 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { public static int getDataLength(TransactionData transactionData) throws TransformationException { IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) transactionData; - return TYPE_LENGTH + TYPELESS_LENGTH + issueAssetTransactionData.getAssetName().length() + issueAssetTransactionData.getDescription().length(); + return TYPE_LENGTH + TYPELESS_LENGTH + Utf8.encodedLength(issueAssetTransactionData.getAssetName()) + + Utf8.encodedLength(issueAssetTransactionData.getDescription()); } public static byte[] toBytes(TransactionData transactionData) throws TransformationException { diff --git a/src/transform/transaction/RegisterNameTransactionTransformer.java b/src/transform/transaction/RegisterNameTransactionTransformer.java index ea893f86..22904335 100644 --- a/src/transform/transaction/RegisterNameTransactionTransformer.java +++ b/src/transform/transaction/RegisterNameTransactionTransformer.java @@ -7,6 +7,7 @@ import java.nio.ByteBuffer; import org.json.simple.JSONObject; +import com.google.common.base.Utf8; import com.google.common.hash.HashCode; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; @@ -60,8 +61,8 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer { public static int getDataLength(TransactionData transactionData) throws TransformationException { RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData; - int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + registerNameTransactionData.getName().length() - + registerNameTransactionData.getData().length(); + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(registerNameTransactionData.getName()) + + Utf8.encodedLength(registerNameTransactionData.getData()); return dataLength; } diff --git a/src/transform/transaction/SellNameTransactionTransformer.java b/src/transform/transaction/SellNameTransactionTransformer.java index 2b070beb..b8156fd2 100644 --- a/src/transform/transaction/SellNameTransactionTransformer.java +++ b/src/transform/transaction/SellNameTransactionTransformer.java @@ -7,6 +7,7 @@ import java.nio.ByteBuffer; import org.json.simple.JSONObject; +import com.google.common.base.Utf8; import com.google.common.hash.HashCode; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; @@ -57,7 +58,7 @@ public class SellNameTransactionTransformer extends TransactionTransformer { public static int getDataLength(TransactionData transactionData) throws TransformationException { SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData; - int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + sellNameTransactionData.getName().length(); + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(sellNameTransactionData.getName()); return dataLength; } diff --git a/src/transform/transaction/TransactionTransformer.java b/src/transform/transaction/TransactionTransformer.java index bcd643a5..9606b8bb 100644 --- a/src/transform/transaction/TransactionTransformer.java +++ b/src/transform/transaction/TransactionTransformer.java @@ -46,6 +46,9 @@ public class TransactionTransformer extends Transformer { case SELL_NAME: return SellNameTransactionTransformer.fromByteBuffer(byteBuffer); + case CANCEL_SELL_NAME: + return CancelSellNameTransactionTransformer.fromByteBuffer(byteBuffer); + case CREATE_POLL: return CreatePollTransactionTransformer.fromByteBuffer(byteBuffer); @@ -92,6 +95,9 @@ public class TransactionTransformer extends Transformer { case SELL_NAME: return SellNameTransactionTransformer.getDataLength(transactionData); + case CANCEL_SELL_NAME: + return CancelSellNameTransactionTransformer.getDataLength(transactionData); + case CREATE_POLL: return CreatePollTransactionTransformer.getDataLength(transactionData); @@ -138,6 +144,9 @@ public class TransactionTransformer extends Transformer { case SELL_NAME: return SellNameTransactionTransformer.toBytes(transactionData); + case CANCEL_SELL_NAME: + return CancelSellNameTransactionTransformer.toBytes(transactionData); + case CREATE_POLL: return CreatePollTransactionTransformer.toBytes(transactionData); @@ -184,6 +193,9 @@ public class TransactionTransformer extends Transformer { case SELL_NAME: return SellNameTransactionTransformer.toJSON(transactionData); + case CANCEL_SELL_NAME: + return CancelSellNameTransactionTransformer.toJSON(transactionData); + case CREATE_POLL: return CreatePollTransactionTransformer.toJSON(transactionData); diff --git a/src/transform/transaction/UpdateNameTransactionTransformer.java b/src/transform/transaction/UpdateNameTransactionTransformer.java index 56f88aba..d612c1fc 100644 --- a/src/transform/transaction/UpdateNameTransactionTransformer.java +++ b/src/transform/transaction/UpdateNameTransactionTransformer.java @@ -7,6 +7,7 @@ import java.nio.ByteBuffer; import org.json.simple.JSONObject; +import com.google.common.base.Utf8; import com.google.common.hash.HashCode; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; @@ -60,8 +61,8 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer { public static int getDataLength(TransactionData transactionData) throws TransformationException { UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData; - int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + updateNameTransactionData.getName().length() - + updateNameTransactionData.getNewData().length(); + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(updateNameTransactionData.getName()) + + Utf8.encodedLength(updateNameTransactionData.getNewData()); return dataLength; } diff --git a/src/transform/transaction/VoteOnPollTransactionTransformer.java b/src/transform/transaction/VoteOnPollTransactionTransformer.java index 7cc82756..6c45d828 100644 --- a/src/transform/transaction/VoteOnPollTransactionTransformer.java +++ b/src/transform/transaction/VoteOnPollTransactionTransformer.java @@ -7,6 +7,7 @@ import java.nio.ByteBuffer; import org.json.simple.JSONObject; +import com.google.common.base.Utf8; import com.google.common.hash.HashCode; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; @@ -63,7 +64,7 @@ public class VoteOnPollTransactionTransformer extends TransactionTransformer { public static int getDataLength(TransactionData transactionData) throws TransformationException { VoteOnPollTransactionData voteOnPollTransactionData = (VoteOnPollTransactionData) transactionData; - int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + voteOnPollTransactionData.getPollName().length(); + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(voteOnPollTransactionData.getPollName()); return dataLength; } diff --git a/src/utils/Serialization.java b/src/utils/Serialization.java index ea0c4c5a..38be78ff 100644 --- a/src/utils/Serialization.java +++ b/src/utils/Serialization.java @@ -63,7 +63,8 @@ public class Serialization { } public static void serializeSizedString(ByteArrayOutputStream bytes, String string) throws UnsupportedEncodingException, IOException { - bytes.write(Ints.toByteArray(string.length())); + byte[] stringBytes = string.getBytes("UTF-8"); + bytes.write(Ints.toByteArray(stringBytes.length)); bytes.write(string.getBytes("UTF-8")); }