diff --git a/pom.xml b/pom.xml index 379a7ce8..3f67c2f5 100644 --- a/pom.xml +++ b/pom.xml @@ -504,6 +504,12 @@ mail 1.5.0-b01 + + + net.codebox + homoglyph + 1.2.0 + org.eclipse.jetty diff --git a/src/main/java/org/qortal/data/naming/NameData.java b/src/main/java/org/qortal/data/naming/NameData.java index dffe830e..83681712 100644 --- a/src/main/java/org/qortal/data/naming/NameData.java +++ b/src/main/java/org/qortal/data/naming/NameData.java @@ -12,18 +12,29 @@ import io.swagger.v3.oas.annotations.media.Schema; public class NameData { // Properties - private String owner; + private String name; + + private String reducedName; + + private String owner; + private String data; + private long registered; - private Long updated; - // No need to expose this via API + + private Long updated; // Not always present + + private boolean isForSale; + + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + private Long salePrice; + + // For internal use - no need to expose this via API @XmlTransient @Schema(hidden = true) private byte[] reference; - private boolean isForSale; - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - private Long salePrice; + // For internal use @XmlTransient @Schema(hidden = true) @@ -31,14 +42,17 @@ public class NameData { // Constructors - // necessary for JAX-RS serialization + // necessary for JAXB protected NameData() { } - public NameData(String owner, String name, String data, long registered, Long updated, byte[] reference, boolean isForSale, Long salePrice, - int creationGroupId) { - this.owner = owner; + // Typically used when fetching from repository + public NameData(String name, String reducedName, String owner, String data, long registered, + Long updated, boolean isForSale, Long salePrice, + byte[] reference, int creationGroupId) { this.name = name; + this.reducedName = reducedName; + this.owner = owner; this.data = data; this.registered = registered; this.updated = updated; @@ -48,12 +62,29 @@ public class NameData { this.creationGroupId = creationGroupId; } - public NameData(String owner, String name, String data, long registered, byte[] reference, int creationGroupId) { - this(owner, name, data, registered, null, reference, false, null, creationGroupId); + // Typically used when registering a new name + public NameData(String name, String reducedName, String owner, String data, long registered, byte[] reference, int creationGroupId) { + this(name, reducedName, owner, data, registered, null, false, null, reference, creationGroupId); } // Getters / setters + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getReducedName() { + return this.reducedName; + } + + public void setReducedName(String reducedName) { + this.reducedName = reducedName; + } + public String getOwner() { return this.owner; } @@ -62,10 +93,6 @@ public class NameData { this.owner = owner; } - public String getName() { - return this.name; - } - public String getData() { return this.data; } @@ -86,15 +113,7 @@ public class NameData { this.updated = updated; } - public byte[] getReference() { - return this.reference; - } - - public void setReference(byte[] reference) { - this.reference = reference; - } - - public boolean getIsForSale() { + public boolean isForSale() { return this.isForSale; } @@ -110,6 +129,14 @@ public class NameData { this.salePrice = salePrice; } + public byte[] getReference() { + return this.reference; + } + + public void setReference(byte[] reference) { + this.reference = reference; + } + public int getCreationGroupId() { return this.creationGroupId; } diff --git a/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java b/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java index 37cc65d2..6f0ef214 100644 --- a/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java @@ -3,8 +3,10 @@ package org.qortal.data.transaction; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlTransient; import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue; +import org.qortal.naming.Name; import org.qortal.transaction.Transaction.TransactionType; import io.swagger.v3.oas.annotations.media.Schema; @@ -17,15 +19,21 @@ import io.swagger.v3.oas.annotations.media.Schema; public class RegisterNameTransactionData extends TransactionData { // Properties + @Schema(description = "registrant's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] registrantPublicKey; - @Schema(description = "new owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v") - private String owner; + @Schema(description = "requested name", example = "my-name") private String name; + @Schema(description = "simple name-related info in JSON format", example = "{ \"age\": 30 }") private String data; + // For internal use + @XmlTransient + @Schema(hidden = true) + private String reducedName; + // Constructors // For JAXB @@ -38,13 +46,18 @@ public class RegisterNameTransactionData extends TransactionData { } /** From repository */ - public RegisterNameTransactionData(BaseTransactionData baseTransactionData, String owner, String name, String data) { + public RegisterNameTransactionData(BaseTransactionData baseTransactionData, String name, String data, String reducedName) { super(TransactionType.REGISTER_NAME, baseTransactionData); this.registrantPublicKey = baseTransactionData.creatorPublicKey; - this.owner = owner; this.name = name; this.data = data; + this.reducedName = reducedName; + } + + /** From network */ + public RegisterNameTransactionData(BaseTransactionData baseTransactionData, String name, String data) { + this(baseTransactionData, name, data, Name.reduceName(name)); } // Getters / setters @@ -53,10 +66,6 @@ public class RegisterNameTransactionData extends TransactionData { return this.registrantPublicKey; } - public String getOwner() { - return this.owner; - } - public String getName() { return this.name; } @@ -65,4 +74,12 @@ public class RegisterNameTransactionData extends TransactionData { return this.data; } + public String getReducedName() { + return this.reducedName; + } + + public void setReducedName(String reducedName) { + this.reducedName = reducedName; + } + } diff --git a/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java b/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java index 70c7404d..5d7e449a 100644 --- a/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java @@ -5,6 +5,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlTransient; +import org.qortal.naming.Name; import org.qortal.transaction.Transaction.TransactionType; import io.swagger.v3.oas.annotations.media.Schema; @@ -15,14 +16,24 @@ import io.swagger.v3.oas.annotations.media.Schema; public class UpdateNameTransactionData extends TransactionData { // Properties + @Schema(description = "owner's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] ownerPublicKey; - @Schema(description = "new owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v") - private String newOwner; + @Schema(description = "which name to update", example = "my-name") private String name; + + @Schema(description = "new name", example = "my-new-name") + private String newName; + @Schema(description = "replacement simple name-related info in JSON format", example = "{ \"age\": 30 }") private String newData; + + // For internal use + @XmlTransient + @Schema(hidden = true) + private String reducedNewName; + // For internal use when orphaning @XmlTransient @Schema(hidden = true) @@ -40,19 +51,20 @@ public class UpdateNameTransactionData extends TransactionData { } /** From repository */ - public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String newOwner, String name, String newData, byte[] nameReference) { + public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String name, String newName, String newData, String reducedNewName, byte[] nameReference) { super(TransactionType.UPDATE_NAME, baseTransactionData); this.ownerPublicKey = baseTransactionData.creatorPublicKey; - this.newOwner = newOwner; this.name = name; + this.newName = newName; this.newData = newData; + this.reducedNewName = reducedNewName; this.nameReference = nameReference; } /** From network/API */ - public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String newOwner, String name, String newData) { - this(baseTransactionData, newOwner, name, newData, null); + public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String name, String newName, String newData) { + this(baseTransactionData, name, newName, newData, Name.reduceName(newName), null); } // Getters / setters @@ -61,18 +73,26 @@ public class UpdateNameTransactionData extends TransactionData { return this.ownerPublicKey; } - public String getNewOwner() { - return this.newOwner; - } - public String getName() { return this.name; } + public String getNewName() { + return this.newName; + } + public String getNewData() { return this.newData; } + public String getReducedNewName() { + return this.reducedNewName; + } + + public void setReducedNewName(String reducedNewName) { + this.reducedNewName = reducedNewName; + } + public byte[] getNameReference() { return this.nameReference; } diff --git a/src/main/java/org/qortal/naming/Name.java b/src/main/java/org/qortal/naming/Name.java index b377907f..b7570552 100644 --- a/src/main/java/org/qortal/naming/Name.java +++ b/src/main/java/org/qortal/naming/Name.java @@ -3,6 +3,7 @@ package org.qortal.naming; import org.qortal.account.Account; import org.qortal.account.PublicKeyAccount; import org.qortal.asset.Asset; +import org.qortal.crypto.Crypto; import org.qortal.data.naming.NameData; import org.qortal.data.transaction.BuyNameTransactionData; import org.qortal.data.transaction.CancelSellNameTransactionData; @@ -12,6 +13,8 @@ import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.UpdateNameTransactionData; import org.qortal.repository.DataException; import org.qortal.repository.Repository; +import org.qortal.transaction.Transaction.TransactionType; +import org.qortal.utils.Unicode; public class Name { @@ -20,7 +23,8 @@ public class Name { private NameData nameData; // Useful constants - public static final int MAX_NAME_SIZE = 400; + public static final int MIN_NAME_SIZE = 3; + public static final int MAX_NAME_SIZE = 40; public static final int MAX_DATA_SIZE = 4000; // Constructors @@ -33,8 +37,12 @@ public class Name { */ public Name(Repository repository, RegisterNameTransactionData registerNameTransactionData) { this.repository = repository; - this.nameData = new NameData(registerNameTransactionData.getOwner(), - registerNameTransactionData.getName(), registerNameTransactionData.getData(), registerNameTransactionData.getTimestamp(), + + String owner = Crypto.toAddress(registerNameTransactionData.getRegistrantPublicKey()); + String reducedName = Unicode.sanitize(registerNameTransactionData.getName()); + + this.nameData = new NameData(registerNameTransactionData.getName(), reducedName, owner, + registerNameTransactionData.getData(), registerNameTransactionData.getTimestamp(), registerNameTransactionData.getSignature(), registerNameTransactionData.getTxGroupId()); } @@ -52,6 +60,10 @@ public class Name { // Processing + public static String reduceName(String name) { + return Unicode.sanitize(name); + } + public void register() throws DataException { this.repository.getNameRepository().save(this.nameData); } @@ -60,35 +72,6 @@ public class Name { this.repository.getNameRepository().delete(this.nameData.getName()); } - private void revert() throws DataException { - TransactionData previousTransactionData = this.repository.getTransactionRepository().fromSignature(this.nameData.getReference()); - if (previousTransactionData == null) - throw new DataException("Unable to revert name transaction as referenced transaction not found in repository"); - - switch (previousTransactionData.getType()) { - case REGISTER_NAME: - RegisterNameTransactionData previousRegisterNameTransactionData = (RegisterNameTransactionData) previousTransactionData; - this.nameData.setOwner(previousRegisterNameTransactionData.getOwner()); - this.nameData.setData(previousRegisterNameTransactionData.getData()); - break; - - case UPDATE_NAME: - UpdateNameTransactionData previousUpdateNameTransactionData = (UpdateNameTransactionData) previousTransactionData; - this.nameData.setData(previousUpdateNameTransactionData.getNewData()); - this.nameData.setOwner(previousUpdateNameTransactionData.getNewOwner()); - break; - - case BUY_NAME: - BuyNameTransactionData previousBuyNameTransactionData = (BuyNameTransactionData) previousTransactionData; - Account buyer = new PublicKeyAccount(this.repository, previousBuyNameTransactionData.getBuyerPublicKey()); - this.nameData.setOwner(buyer.getAddress()); - break; - - default: - throw new IllegalStateException("Unable to revert name transaction due to unsupported referenced transaction"); - } - } - public void update(UpdateNameTransactionData updateNameTransactionData) throws DataException { // Update reference in transaction data updateNameTransactionData.setNameReference(this.nameData.getReference()); @@ -96,9 +79,19 @@ public class Name { // New name reference is this transaction's signature this.nameData.setReference(updateNameTransactionData.getSignature()); - // Update Name's owner and data - this.nameData.setOwner(updateNameTransactionData.getNewOwner()); - this.nameData.setData(updateNameTransactionData.getNewData()); + // Set name's last-updated timestamp + this.nameData.setUpdated(updateNameTransactionData.getTimestamp()); + + // Update name and data where appropriate + if (!updateNameTransactionData.getNewName().isEmpty()) { + this.nameData.setName(updateNameTransactionData.getNewName()); + + // If we're changing the name, we need to delete old entry + this.repository.getNameRepository().delete(updateNameTransactionData.getName()); + } + + if (!updateNameTransactionData.getNewData().isEmpty()) + this.nameData.setData(updateNameTransactionData.getNewData()); // Save updated name data this.repository.getNameRepository().save(this.nameData); @@ -106,18 +99,68 @@ public class Name { public void revert(UpdateNameTransactionData updateNameTransactionData) throws DataException { // Previous name reference is taken from this transaction's cached copy - this.nameData.setReference(updateNameTransactionData.getNameReference()); + byte[] nameReference = updateNameTransactionData.getNameReference(); - // Previous Name's owner and/or data taken from referenced transaction - this.revert(); + // Revert name's name-changing transaction reference + this.nameData.setReference(nameReference); + + // Revert name's last-updated timestamp + this.nameData.setUpdated(fetchPreviousUpdateTimestamp(nameReference)); + + // We can find previous 'name' from update transaction + this.nameData.setName(updateNameTransactionData.getName()); + + // We might need to hunt for previous data value + if (!updateNameTransactionData.getNewData().isEmpty()) + this.nameData.setData(findPreviousData(nameReference)); - // Save reverted name data this.repository.getNameRepository().save(this.nameData); + if (!updateNameTransactionData.getNewName().isEmpty()) + // Name has changed, delete old entry + this.repository.getNameRepository().delete(updateNameTransactionData.getNewName()); + // Remove reference to previous name-changing transaction updateNameTransactionData.setNameReference(null); } + private String findPreviousData(byte[] nameReference) throws DataException { + // Follow back through name-references until we hit the data we need + while (true) { + TransactionData previousTransactionData = this.repository.getTransactionRepository().fromSignature(nameReference); + if (previousTransactionData == null) + throw new DataException("Unable to revert name transaction as referenced transaction not found in repository"); + + switch (previousTransactionData.getType()) { + case REGISTER_NAME: { + RegisterNameTransactionData previousRegisterNameTransactionData = (RegisterNameTransactionData) previousTransactionData; + + return previousRegisterNameTransactionData.getData(); + } + + case UPDATE_NAME: { + UpdateNameTransactionData previousUpdateNameTransactionData = (UpdateNameTransactionData) previousTransactionData; + + if (!previousUpdateNameTransactionData.getNewData().isEmpty()) + return previousUpdateNameTransactionData.getNewData(); + + nameReference = previousUpdateNameTransactionData.getNameReference(); + + break; + } + + case BUY_NAME: { + BuyNameTransactionData previousBuyNameTransactionData = (BuyNameTransactionData) previousTransactionData; + nameReference = previousBuyNameTransactionData.getNameReference(); + break; + } + + default: + throw new IllegalStateException("Unable to revert name transaction due to unsupported referenced transaction"); + } + } + } + public void sell(SellNameTransactionData sellNameTransactionData) throws DataException { // Mark as for-sale and set price this.nameData.setIsForSale(true); @@ -153,6 +196,10 @@ public class Name { } public void buy(BuyNameTransactionData buyNameTransactionData) throws DataException { + // Save previous name-changing reference in this transaction's data + // Caller is expected to save + buyNameTransactionData.setNameReference(this.nameData.getReference()); + // Mark not for-sale but leave price in case we want to orphan this.nameData.setIsForSale(false); @@ -166,12 +213,12 @@ public class Name { // Update buyer's balance buyer.modifyAssetBalance(Asset.QORT, - buyNameTransactionData.getAmount()); - // Update reference in transaction data - buyNameTransactionData.setNameReference(this.nameData.getReference()); - - // New name reference is this transaction's signature + // Set name-changing reference to this transaction this.nameData.setReference(buyNameTransactionData.getSignature()); + // Set name's last-updated timestamp + this.nameData.setUpdated(buyNameTransactionData.getTimestamp()); + // Save updated name data this.repository.getNameRepository().save(this.nameData); } @@ -181,25 +228,41 @@ public class Name { this.nameData.setIsForSale(true); this.nameData.setSalePrice(buyNameTransactionData.getAmount()); - // Previous name reference is taken from this transaction's cached copy + // Previous name-changing reference is taken from this transaction's cached copy this.nameData.setReference(buyNameTransactionData.getNameReference()); - // Remove reference in transaction data - buyNameTransactionData.setNameReference(null); + // Revert name's last-updated timestamp + this.nameData.setUpdated(fetchPreviousUpdateTimestamp(buyNameTransactionData.getNameReference())); + + // Revert to previous owner + this.nameData.setOwner(buyNameTransactionData.getSeller()); + + // Save updated name data + this.repository.getNameRepository().save(this.nameData); // Revert buyer's balance Account buyer = new PublicKeyAccount(this.repository, buyNameTransactionData.getBuyerPublicKey()); buyer.modifyAssetBalance(Asset.QORT, buyNameTransactionData.getAmount()); - // Previous Name's owner and/or data taken from referenced transaction - this.revert(); - // Revert seller's balance - Account seller = new Account(this.repository, this.nameData.getOwner()); + Account seller = new Account(this.repository, buyNameTransactionData.getSeller()); seller.modifyAssetBalance(Asset.QORT, - buyNameTransactionData.getAmount()); - // Save reverted name data - this.repository.getNameRepository().save(this.nameData); + // Clean previous name-changing reference from this transaction's data + // Caller is expected to save + buyNameTransactionData.setNameReference(null); + } + + private Long fetchPreviousUpdateTimestamp(byte[] nameReference) throws DataException { + TransactionData previousTransactionData = this.repository.getTransactionRepository().fromSignature(nameReference); + if (previousTransactionData == null) + throw new DataException("Unable to revert name transaction as referenced transaction not found in repository"); + + // If we've hit REGISTER_NAME then we've run out of updates + if (previousTransactionData.getType() == TransactionType.REGISTER_NAME) + return null; + + return previousTransactionData.getTimestamp(); } } diff --git a/src/main/java/org/qortal/repository/NameRepository.java b/src/main/java/org/qortal/repository/NameRepository.java index e056fb1d..d6c0f33e 100644 --- a/src/main/java/org/qortal/repository/NameRepository.java +++ b/src/main/java/org/qortal/repository/NameRepository.java @@ -10,6 +10,10 @@ public interface NameRepository { public boolean nameExists(String name) throws DataException; + public NameData fromReducedName(String reducedName) throws DataException; + + public boolean reducedNameExists(String reducedName) throws DataException; + public List getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException; public default List getAllNames() throws DataException { diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 730740cd..e4d7f81a 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -285,20 +285,24 @@ public class HSQLDBDatabaseUpdates { case 8: // Name-related - stmt.execute("CREATE TABLE Names (name RegisteredName, owner QortalAddress NOT NULL, " - + "registered_when EpochMillis NOT NULL, updated_when EpochMillis, creation_group_id GroupID NOT NULL DEFAULT 0, " - + "reference Signature, is_for_sale BOOLEAN NOT NULL DEFAULT FALSE, sale_price QortalAmount, data NameData NOT NULL, " + stmt.execute("CREATE TABLE Names (name RegisteredName, reduced_name RegisteredName, owner QortalAddress NOT NULL, " + + "registered_when EpochMillis NOT NULL, updated_when EpochMillis, " + + "is_for_sale BOOLEAN NOT NULL DEFAULT FALSE, sale_price QortalAmount, data NameData NOT NULL, " + + "reference Signature, creation_group_id GroupID NOT NULL DEFAULT 0, " + "PRIMARY KEY (name))"); // For finding names by owner stmt.execute("CREATE INDEX NamesOwnerIndex ON Names (owner)"); + // For finding names by 'reduced' form + stmt.execute("CREATE INDEX NamesReducedNameIndex ON Names (reduced_name)"); // Register Name Transactions stmt.execute("CREATE TABLE RegisterNameTransactions (signature Signature, registrant QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "owner QortalAddress NOT NULL, data NameData NOT NULL, " + TRANSACTION_KEYS + ")"); + + "data NameData NOT NULL, reduced_name RegisteredName NOT NULL, " + TRANSACTION_KEYS + ")"); // Update Name Transactions stmt.execute("CREATE TABLE UpdateNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "new_owner QortalAddress NOT NULL, new_data NameData NOT NULL, name_reference Signature, " + TRANSACTION_KEYS + ")"); + + "new_name RegisteredName NOT NULL, new_data NameData NOT NULL, reduced_new_name RegisteredName NOT NULL, " + + "name_reference Signature, " + TRANSACTION_KEYS + ")"); // Sell Name Transactions stmt.execute("CREATE TABLE SellNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java index 3f6c9030..3a3574ef 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java @@ -19,31 +19,33 @@ public class HSQLDBNameRepository implements NameRepository { @Override public NameData fromName(String name) throws DataException { - String sql = "SELECT owner, data, registered_when, updated_when, reference, is_for_sale, sale_price, creation_group_id FROM Names WHERE name = ?"; + String sql = "SELECT reduced_name, owner, data, registered_when, updated_when, " + + "is_for_sale, sale_price, reference, creation_group_id FROM Names WHERE name = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, name)) { if (resultSet == null) return null; - String owner = resultSet.getString(1); - String data = resultSet.getString(2); - long registered = resultSet.getLong(3); + String reducedName = resultSet.getString(1); + String owner = resultSet.getString(2); + String data = resultSet.getString(3); + long registered = resultSet.getLong(4); // Special handling for possibly-NULL "updated" column - Long updated = resultSet.getLong(4); + Long updated = resultSet.getLong(5); if (updated == 0 && resultSet.wasNull()) updated = null; - byte[] reference = resultSet.getBytes(5); boolean isForSale = resultSet.getBoolean(6); Long salePrice = resultSet.getLong(7); if (salePrice == 0 && resultSet.wasNull()) salePrice = null; - int creationGroupId = resultSet.getInt(8); + byte[] reference = resultSet.getBytes(8); + int creationGroupId = resultSet.getInt(9); - return new NameData(owner, name, data, registered, updated, reference, isForSale, salePrice, creationGroupId); + return new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId); } catch (SQLException e) { throw new DataException("Unable to fetch name info from repository", e); } @@ -58,11 +60,55 @@ public class HSQLDBNameRepository implements NameRepository { } } + @Override + public NameData fromReducedName(String reducedName) throws DataException { + String sql = "SELECT name, owner, data, registered_when, updated_when, " + + "is_for_sale, sale_price, reference, creation_group_id FROM Names WHERE reduced_name = ?"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, reducedName)) { + if (resultSet == null) + return null; + + String name = resultSet.getString(1); + String owner = resultSet.getString(2); + String data = resultSet.getString(3); + long registered = resultSet.getLong(4); + + // Special handling for possibly-NULL "updated" column + Long updated = resultSet.getLong(5); + if (updated == 0 && resultSet.wasNull()) + updated = null; + + boolean isForSale = resultSet.getBoolean(6); + + Long salePrice = resultSet.getLong(7); + if (salePrice == 0 && resultSet.wasNull()) + salePrice = null; + + byte[] reference = resultSet.getBytes(8); + int creationGroupId = resultSet.getInt(9); + + return new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId); + } catch (SQLException e) { + throw new DataException("Unable to fetch name info from repository", e); + } + } + + @Override + public boolean reducedNameExists(String reducedName) throws DataException { + try { + return this.repository.exists("Names", "reduced_name = ?", reducedName); + } catch (SQLException e) { + throw new DataException("Unable to check for reduced name in repository", e); + } + } + @Override public List getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); - sql.append("SELECT name, data, owner, registered_when, updated_when, reference, is_for_sale, sale_price, creation_group_id FROM Names ORDER BY name"); + sql.append("SELECT name, reduced_name, owner, data, registered_when, updated_when, " + + "is_for_sale, sale_price, reference, creation_group_id FROM Names ORDER BY name"); if (reverse != null && reverse) sql.append(" DESC"); @@ -77,25 +123,26 @@ public class HSQLDBNameRepository implements NameRepository { do { String name = resultSet.getString(1); - String data = resultSet.getString(2); + String reducedName = resultSet.getString(2); String owner = resultSet.getString(3); - long registered = resultSet.getLong(4); + String data = resultSet.getString(4); + long registered = resultSet.getLong(5); // Special handling for possibly-NULL "updated" column - Long updated = resultSet.getLong(5); + Long updated = resultSet.getLong(6); if (updated == 0 && resultSet.wasNull()) updated = null; - byte[] reference = resultSet.getBytes(6); boolean isForSale = resultSet.getBoolean(7); Long salePrice = resultSet.getLong(8); if (salePrice == 0 && resultSet.wasNull()) salePrice = null; - int creationGroupId = resultSet.getInt(9); + byte[] reference = resultSet.getBytes(9); + int creationGroupId = resultSet.getInt(10); - names.add(new NameData(owner, name, data, registered, updated, reference, isForSale, salePrice, creationGroupId)); + names.add(new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId)); } while (resultSet.next()); return names; @@ -108,7 +155,8 @@ public class HSQLDBNameRepository implements NameRepository { public List getNamesForSale(Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT name, data, owner, registered_when, updated_when, reference, sale_price, creation_group_id FROM Names WHERE is_for_sale = TRUE ORDER BY name"); + sql.append("SELECT name, reduced_name, owner, data, registered_when, updated_when, " + + "sale_price, reference, creation_group_id FROM Names WHERE is_for_sale = TRUE ORDER BY name"); if (reverse != null && reverse) sql.append(" DESC"); @@ -123,25 +171,26 @@ public class HSQLDBNameRepository implements NameRepository { do { String name = resultSet.getString(1); - String data = resultSet.getString(2); + String reducedName = resultSet.getString(2); String owner = resultSet.getString(3); - long registered = resultSet.getLong(4); + String data = resultSet.getString(4); + long registered = resultSet.getLong(5); // Special handling for possibly-NULL "updated" column - Long updated = resultSet.getLong(5); + Long updated = resultSet.getLong(6); if (updated == 0 && resultSet.wasNull()) updated = null; - byte[] reference = resultSet.getBytes(6); boolean isForSale = true; Long salePrice = resultSet.getLong(7); if (salePrice == 0 && resultSet.wasNull()) salePrice = null; - int creationGroupId = resultSet.getInt(8); + byte[] reference = resultSet.getBytes(8); + int creationGroupId = resultSet.getInt(9); - names.add(new NameData(owner, name, data, registered, updated, reference, isForSale, salePrice, creationGroupId)); + names.add(new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId)); } while (resultSet.next()); return names; @@ -154,7 +203,8 @@ public class HSQLDBNameRepository implements NameRepository { public List getNamesByOwner(String owner, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT name, data, registered_when, updated_when, reference, is_for_sale, sale_price, creation_group_id FROM Names WHERE owner = ? ORDER BY name"); + sql.append("SELECT name, reduced_name, data, registered_when, updated_when, " + + "is_for_sale, sale_price, reference, creation_group_id FROM Names WHERE owner = ? ORDER BY name"); if (reverse != null && reverse) sql.append(" DESC"); @@ -169,24 +219,25 @@ public class HSQLDBNameRepository implements NameRepository { do { String name = resultSet.getString(1); - String data = resultSet.getString(2); - long registered = resultSet.getLong(3); + String reducedName = resultSet.getString(2); + String data = resultSet.getString(3); + long registered = resultSet.getLong(4); // Special handling for possibly-NULL "updated" column - Long updated = resultSet.getLong(4); + Long updated = resultSet.getLong(5); if (updated == 0 && resultSet.wasNull()) updated = null; - byte[] reference = resultSet.getBytes(5); boolean isForSale = resultSet.getBoolean(6); Long salePrice = resultSet.getLong(7); if (salePrice == 0 && resultSet.wasNull()) salePrice = null; - int creationGroupId = resultSet.getInt(8); + byte[] reference = resultSet.getBytes(8); + int creationGroupId = resultSet.getInt(9); - names.add(new NameData(owner, name, data, registered, updated, reference, isForSale, salePrice, creationGroupId)); + names.add(new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId)); } while (resultSet.next()); return names; @@ -223,11 +274,11 @@ public class HSQLDBNameRepository implements NameRepository { public void save(NameData nameData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("Names"); - saveHelper.bind("owner", nameData.getOwner()).bind("name", nameData.getName()).bind("data", nameData.getData()) + saveHelper.bind("name", nameData.getName()).bind("reduced_name", nameData.getReducedName()) + .bind("owner", nameData.getOwner()).bind("data", nameData.getData()) .bind("registered_when", nameData.getRegistered()).bind("updated_when", nameData.getUpdated()) - .bind("reference", nameData.getReference()) - .bind("is_for_sale", nameData.getIsForSale()).bind("sale_price", nameData.getSalePrice()) - .bind("creation_group_id", nameData.getCreationGroupId()); + .bind("is_for_sale", nameData.isForSale()).bind("sale_price", nameData.getSalePrice()) + .bind("reference", nameData.getReference()).bind("creation_group_id", nameData.getCreationGroupId()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBRegisterNameTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBRegisterNameTransactionRepository.java index bddeaf07..82da794f 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBRegisterNameTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBRegisterNameTransactionRepository.java @@ -17,17 +17,17 @@ public class HSQLDBRegisterNameTransactionRepository extends HSQLDBTransactionRe } TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException { - String sql = "SELECT owner, name, data FROM RegisterNameTransactions WHERE signature = ?"; + String sql = "SELECT name, reduced_name, data FROM RegisterNameTransactions WHERE signature = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) { if (resultSet == null) return null; - String owner = resultSet.getString(1); - String name = resultSet.getString(2); + String name = resultSet.getString(1); + String reducedName = resultSet.getString(2); String data = resultSet.getString(3); - return new RegisterNameTransactionData(baseTransactionData, owner, name, data); + return new RegisterNameTransactionData(baseTransactionData, name, data, reducedName); } catch (SQLException e) { throw new DataException("Unable to fetch register name transaction from repository", e); } @@ -40,8 +40,8 @@ public class HSQLDBRegisterNameTransactionRepository extends HSQLDBTransactionRe HSQLDBSaver saveHelper = new HSQLDBSaver("RegisterNameTransactions"); saveHelper.bind("signature", registerNameTransactionData.getSignature()).bind("registrant", registerNameTransactionData.getRegistrantPublicKey()) - .bind("owner", registerNameTransactionData.getOwner()).bind("name", registerNameTransactionData.getName()) - .bind("data", registerNameTransactionData.getData()); + .bind("name", registerNameTransactionData.getName()).bind("data", registerNameTransactionData.getData()) + .bind("reduced_name", registerNameTransactionData.getReducedName()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBUpdateNameTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBUpdateNameTransactionRepository.java index d0df6ff9..447ab5c6 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBUpdateNameTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBUpdateNameTransactionRepository.java @@ -17,18 +17,19 @@ public class HSQLDBUpdateNameTransactionRepository extends HSQLDBTransactionRepo } TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException { - String sql = "SELECT new_owner, name, new_data, name_reference FROM UpdateNameTransactions WHERE signature = ?"; + String sql = "SELECT name, new_name, new_data, reduced_new_name, name_reference FROM UpdateNameTransactions WHERE signature = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) { if (resultSet == null) return null; - String newOwner = resultSet.getString(1); - String name = resultSet.getString(2); + String name = resultSet.getString(1); + String newName = resultSet.getString(2); String newData = resultSet.getString(3); - byte[] nameReference = resultSet.getBytes(4); + String reducedNewName = resultSet.getString(4); + byte[] nameReference = resultSet.getBytes(5); - return new UpdateNameTransactionData(baseTransactionData, newOwner, name, newData, nameReference); + return new UpdateNameTransactionData(baseTransactionData, name, newName, newData, reducedNewName, nameReference); } catch (SQLException e) { throw new DataException("Unable to fetch update name transaction from repository", e); } @@ -41,8 +42,9 @@ public class HSQLDBUpdateNameTransactionRepository extends HSQLDBTransactionRepo HSQLDBSaver saveHelper = new HSQLDBSaver("UpdateNameTransactions"); saveHelper.bind("signature", updateNameTransactionData.getSignature()).bind("owner", updateNameTransactionData.getOwnerPublicKey()) - .bind("new_owner", updateNameTransactionData.getNewOwner()).bind("name", updateNameTransactionData.getName()) - .bind("new_data", updateNameTransactionData.getNewData()).bind("name_reference", updateNameTransactionData.getNameReference()); + .bind("name", updateNameTransactionData.getName()).bind("new_name", updateNameTransactionData.getNewName()) + .bind("new_data", updateNameTransactionData.getNewData()).bind("reduced_new_name", updateNameTransactionData.getReducedNewName()) + .bind("name_reference", updateNameTransactionData.getNameReference()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/transaction/BuyNameTransaction.java b/src/main/java/org/qortal/transaction/BuyNameTransaction.java index 63b6447e..3b58842d 100644 --- a/src/main/java/org/qortal/transaction/BuyNameTransaction.java +++ b/src/main/java/org/qortal/transaction/BuyNameTransaction.java @@ -5,6 +5,7 @@ import java.util.List; import org.qortal.account.Account; import org.qortal.asset.Asset; +import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.naming.NameData; import org.qortal.data.transaction.BuyNameTransactionData; @@ -54,7 +55,7 @@ public class BuyNameTransaction extends Transaction { // Check name size bounds int nameLength = Utf8.encodedLength(name); - if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE) + if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; // Check name is lowercase @@ -68,7 +69,7 @@ public class BuyNameTransaction extends Transaction { return ValidationResult.NAME_DOES_NOT_EXIST; // Check name is currently for sale - if (!nameData.getIsForSale()) + if (!nameData.isForSale()) return ValidationResult.NAME_NOT_FOR_SALE; // Check buyer isn't trying to buy own name @@ -76,6 +77,11 @@ public class BuyNameTransaction extends Transaction { if (buyer.getAddress().equals(nameData.getOwner())) return ValidationResult.BUYER_ALREADY_OWNER; + // If accounts are only allowed one registered name then check for this + if (BlockChain.getInstance().oneNamePerAccount() + && !this.repository.getNameRepository().getNamesByOwner(buyer.getAddress()).isEmpty()) + return ValidationResult.MULTIPLE_NAMES_FORBIDDEN; + // Check expected seller currently owns name if (!this.buyNameTransactionData.getSeller().equals(nameData.getOwner())) return ValidationResult.INVALID_SELLER; @@ -84,7 +90,7 @@ public class BuyNameTransaction extends Transaction { if (this.buyNameTransactionData.getAmount() != nameData.getSalePrice()) return ValidationResult.INVALID_AMOUNT; - // Check issuer has enough funds + // Check buyer has enough funds if (buyer.getConfirmedBalance(Asset.QORT) < this.buyNameTransactionData.getFee()) return ValidationResult.NO_BALANCE; @@ -93,21 +99,21 @@ public class BuyNameTransaction extends Transaction { @Override public void process() throws DataException { - // Update Name + // Buy Name Name name = new Name(this.repository, this.buyNameTransactionData.getName()); name.buy(this.buyNameTransactionData); - // Save transaction with updated "name reference" pointing to previous transaction that updated name + // Save transaction with updated "name reference" pointing to previous transaction that changed name this.repository.getTransactionRepository().save(this.buyNameTransactionData); } @Override public void orphan() throws DataException { - // Revert name + // Un-buy name Name name = new Name(this.repository, this.buyNameTransactionData.getName()); name.unbuy(this.buyNameTransactionData); - // Save this transaction, with removed "name reference" + // Save this transaction, with previous "name reference" this.repository.getTransactionRepository().save(this.buyNameTransactionData); } diff --git a/src/main/java/org/qortal/transaction/CancelSellNameTransaction.java b/src/main/java/org/qortal/transaction/CancelSellNameTransaction.java index 18852c56..da81e4ea 100644 --- a/src/main/java/org/qortal/transaction/CancelSellNameTransaction.java +++ b/src/main/java/org/qortal/transaction/CancelSellNameTransaction.java @@ -62,7 +62,7 @@ public class CancelSellNameTransaction extends Transaction { return ValidationResult.NAME_DOES_NOT_EXIST; // Check name is currently for sale - if (!nameData.getIsForSale()) + if (!nameData.isForSale()) return ValidationResult.NAME_NOT_FOR_SALE; // Check transaction creator matches name's current owner diff --git a/src/main/java/org/qortal/transaction/RegisterNameTransaction.java b/src/main/java/org/qortal/transaction/RegisterNameTransaction.java index 053413eb..c0d91f0b 100644 --- a/src/main/java/org/qortal/transaction/RegisterNameTransaction.java +++ b/src/main/java/org/qortal/transaction/RegisterNameTransaction.java @@ -6,12 +6,12 @@ import java.util.List; import org.qortal.account.Account; import org.qortal.asset.Asset; import org.qortal.block.BlockChain; -import org.qortal.crypto.Crypto; import org.qortal.data.transaction.RegisterNameTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.naming.Name; import org.qortal.repository.DataException; import org.qortal.repository.Repository; +import org.qortal.utils.Unicode; import com.google.common.base.Utf8; @@ -32,7 +32,7 @@ public class RegisterNameTransaction extends Transaction { @Override public List getRecipientAddresses() throws DataException { - return Collections.singletonList(this.registerNameTransactionData.getOwner()); + return Collections.emptyList(); } // Navigation @@ -41,47 +41,55 @@ public class RegisterNameTransaction extends Transaction { return this.getCreator(); } + private synchronized String getReducedName() { + if (this.registerNameTransactionData.getReducedName() == null) { + String reducedName = Name.reduceName(this.registerNameTransactionData.getName()); + this.registerNameTransactionData.setReducedName(reducedName); + } + + return this.registerNameTransactionData.getReducedName(); + } + // Processing @Override public ValidationResult isValid() throws DataException { Account registrant = getRegistrant(); - - // Check owner address is valid - if (!Crypto.isValidAddress(this.registerNameTransactionData.getOwner())) - return ValidationResult.INVALID_ADDRESS; + String name = this.registerNameTransactionData.getName(); // Check name size bounds - int nameLength = Utf8.encodedLength(this.registerNameTransactionData.getName()); - if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE) + int nameLength = Utf8.encodedLength(name); + if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; // Check data size bounds int dataLength = Utf8.encodedLength(this.registerNameTransactionData.getData()); - if (dataLength < 1 || dataLength > Name.MAX_DATA_SIZE) + if (dataLength > Name.MAX_DATA_SIZE) return ValidationResult.INVALID_DATA_LENGTH; - // Check name is lowercase - if (!this.registerNameTransactionData.getName().equals(this.registerNameTransactionData.getName().toLowerCase())) + // Check name is in normalized form (no leading/trailing whitespace, etc.) + if (!name.equals(Unicode.normalize(name))) return ValidationResult.NAME_NOT_LOWER_CASE; // Check registrant has enough funds if (registrant.getConfirmedBalance(Asset.QORT) < this.registerNameTransactionData.getFee()) return ValidationResult.NO_BALANCE; + // Fill in missing reduced name. Caller is likely to save this as next step. + getReducedName(); + return ValidationResult.OK; } @Override public ValidationResult isProcessable() throws DataException { // Check the name isn't already taken - if (this.repository.getNameRepository().nameExists(this.registerNameTransactionData.getName())) + if (this.repository.getNameRepository().reducedNameExists(getReducedName())) return ValidationResult.NAME_ALREADY_REGISTERED; - Account registrant = getRegistrant(); - // If accounts are only allowed one registered name then check for this - if (BlockChain.getInstance().oneNamePerAccount() && !this.repository.getNameRepository().getNamesByOwner(registrant.getAddress()).isEmpty()) + if (BlockChain.getInstance().oneNamePerAccount() + && !this.repository.getNameRepository().getNamesByOwner(getRegistrant().getAddress()).isEmpty()) return ValidationResult.MULTIPLE_NAMES_FORBIDDEN; return ValidationResult.OK; diff --git a/src/main/java/org/qortal/transaction/SellNameTransaction.java b/src/main/java/org/qortal/transaction/SellNameTransaction.java index 8694e66d..5b77097f 100644 --- a/src/main/java/org/qortal/transaction/SellNameTransaction.java +++ b/src/main/java/org/qortal/transaction/SellNameTransaction.java @@ -65,7 +65,7 @@ public class SellNameTransaction extends Transaction { return ValidationResult.NAME_DOES_NOT_EXIST; // Check name isn't currently for sale - if (nameData.getIsForSale()) + if (nameData.isForSale()) return ValidationResult.NAME_ALREADY_FOR_SALE; // Check transaction's public key matches name's current owner diff --git a/src/main/java/org/qortal/transaction/UpdateNameTransaction.java b/src/main/java/org/qortal/transaction/UpdateNameTransaction.java index 2b731bd1..f4cd3020 100644 --- a/src/main/java/org/qortal/transaction/UpdateNameTransaction.java +++ b/src/main/java/org/qortal/transaction/UpdateNameTransaction.java @@ -5,13 +5,13 @@ import java.util.List; import org.qortal.account.Account; import org.qortal.asset.Asset; -import org.qortal.crypto.Crypto; import org.qortal.data.naming.NameData; import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.UpdateNameTransactionData; import org.qortal.naming.Name; import org.qortal.repository.DataException; import org.qortal.repository.Repository; +import org.qortal.utils.Unicode; import com.google.common.base.Utf8; @@ -32,7 +32,7 @@ public class UpdateNameTransaction extends Transaction { @Override public List getRecipientAddresses() throws DataException { - return Collections.singletonList(this.updateNameTransactionData.getNewOwner()); + return Collections.emptyList(); } // Navigation @@ -41,8 +41,13 @@ public class UpdateNameTransaction extends Transaction { return this.getCreator(); } - public Account getNewOwner() { - return new Account(this.repository, this.updateNameTransactionData.getNewOwner()); + private synchronized String getReducedNewName() { + if (this.updateNameTransactionData.getReducedNewName() == null) { + String reducedNewName = Name.reduceName(this.updateNameTransactionData.getNewName()); + this.updateNameTransactionData.setReducedNewName(reducedNewName); + } + + return this.updateNameTransactionData.getReducedNewName(); } // Processing @@ -51,22 +56,13 @@ public class UpdateNameTransaction extends Transaction { public ValidationResult isValid() throws DataException { String name = this.updateNameTransactionData.getName(); - // Check new owner address is valid - if (!Crypto.isValidAddress(this.updateNameTransactionData.getNewOwner())) - return ValidationResult.INVALID_ADDRESS; - // Check name size bounds int nameLength = Utf8.encodedLength(name); - if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE) + if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; - // Check new data size bounds - int newDataLength = Utf8.encodedLength(this.updateNameTransactionData.getNewData()); - if (newDataLength < 1 || newDataLength > Name.MAX_DATA_SIZE) - return ValidationResult.INVALID_DATA_LENGTH; - - // Check name is lowercase - if (!name.equals(name.toLowerCase())) + // Check name is in normalized form (no leading/trailing whitespace, etc.) + if (!name.equals(Unicode.normalize(name))) return ValidationResult.NAME_NOT_LOWER_CASE; NameData nameData = this.repository.getNameRepository().fromName(name); @@ -79,12 +75,33 @@ public class UpdateNameTransaction extends Transaction { if (nameData.getCreationGroupId() != this.updateNameTransactionData.getTxGroupId()) return ValidationResult.TX_GROUP_ID_MISMATCH; + // Check new name (0 length means don't update name) + String newName = this.updateNameTransactionData.getNewName(); + int newNameLength = Utf8.encodedLength(newName); + if (newNameLength != 0) { + // Check new name size bounds + if (newNameLength < Name.MIN_NAME_SIZE || newNameLength > Name.MAX_NAME_SIZE) + return ValidationResult.INVALID_NAME_LENGTH; + + // Check new name is in normalized form (no leading/trailing whitespace, etc.) + if (!newName.equals(Unicode.normalize(newName))) + return ValidationResult.NAME_NOT_LOWER_CASE; + } + + // Check new data size bounds (0 length means don't update data) + int newDataLength = Utf8.encodedLength(this.updateNameTransactionData.getNewData()); + if (newDataLength > Name.MAX_DATA_SIZE) + return ValidationResult.INVALID_DATA_LENGTH; + Account owner = getOwner(); // Check owner has enough funds if (owner.getConfirmedBalance(Asset.QORT) < this.updateNameTransactionData.getFee()) return ValidationResult.NO_BALANCE; + // Fill in missing reduced new name. Caller is likely to save this as next step. + getReducedNewName(); + return ValidationResult.OK; } @@ -92,8 +109,12 @@ public class UpdateNameTransaction extends Transaction { public ValidationResult isProcessable() throws DataException { NameData nameData = this.repository.getNameRepository().fromName(this.updateNameTransactionData.getName()); + // Check name still exists + if (nameData == null) + return ValidationResult.NAME_DOES_NOT_EXIST; + // Check name isn't currently for sale - if (nameData.getIsForSale()) + if (nameData.isForSale()) return ValidationResult.NAME_ALREADY_FOR_SALE; Account owner = getOwner(); @@ -102,6 +123,11 @@ public class UpdateNameTransaction extends Transaction { if (!owner.getAddress().equals(nameData.getOwner())) return ValidationResult.INVALID_NAME_OWNER; + // Check new name isn't already taken, unless it is the same name (this allows for case-adjusting renames) + NameData newNameData = this.repository.getNameRepository().fromReducedName(getReducedNewName()); + if (newNameData != null && !newNameData.getName().equals(nameData.getName())) + return ValidationResult.NAME_ALREADY_REGISTERED; + return ValidationResult.OK; } @@ -111,17 +137,22 @@ public class UpdateNameTransaction extends Transaction { Name name = new Name(this.repository, this.updateNameTransactionData.getName()); name.update(this.updateNameTransactionData); - // Save this transaction, now with updated "name reference" to previous transaction that updated name + // Save this transaction, now with updated "name reference" to previous transaction that changed name this.repository.getTransactionRepository().save(this.updateNameTransactionData); } @Override public void orphan() throws DataException { - // Revert name - Name name = new Name(this.repository, this.updateNameTransactionData.getName()); + // Revert update + + String nameToRevert = this.updateNameTransactionData.getNewName(); + if (nameToRevert.isEmpty()) + nameToRevert = this.updateNameTransactionData.getName(); + + Name name = new Name(this.repository, nameToRevert); name.revert(this.updateNameTransactionData); - // Save this transaction, now with removed "name reference" + // Save this transaction, with previous "name reference" this.repository.getTransactionRepository().save(this.updateNameTransactionData); } diff --git a/src/main/java/org/qortal/transform/transaction/RegisterNameTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/RegisterNameTransactionTransformer.java index 1bf4057e..6722c601 100644 --- a/src/main/java/org/qortal/transform/transaction/RegisterNameTransactionTransformer.java +++ b/src/main/java/org/qortal/transform/transaction/RegisterNameTransactionTransformer.java @@ -33,7 +33,6 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer { layout.add("transaction's groupID", TransformationType.INT); layout.add("reference", TransformationType.SIGNATURE); layout.add("name registrant's public key", TransformationType.PUBLIC_KEY); - layout.add("name owner", TransformationType.ADDRESS); layout.add("name length", TransformationType.INT); layout.add("name", TransformationType.STRING); layout.add("data length", TransformationType.INT); @@ -52,8 +51,6 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer { byte[] registrantPublicKey = Serialization.deserializePublicKey(byteBuffer); - String owner = Serialization.deserializeAddress(byteBuffer); - String name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE); String data = Serialization.deserializeSizedString(byteBuffer, Name.MAX_DATA_SIZE); @@ -65,7 +62,7 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer { BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, registrantPublicKey, fee, signature); - return new RegisterNameTransactionData(baseTransactionData, owner, name, data); + return new RegisterNameTransactionData(baseTransactionData, name, data); } public static int getDataLength(TransactionData transactionData) throws TransformationException { @@ -83,8 +80,6 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer { transformCommonBytes(transactionData, bytes); - Serialization.serializeAddress(bytes, registerNameTransactionData.getOwner()); - Serialization.serializeSizedString(bytes, registerNameTransactionData.getName()); Serialization.serializeSizedString(bytes, registerNameTransactionData.getData()); diff --git a/src/main/java/org/qortal/transform/transaction/UpdateNameTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/UpdateNameTransactionTransformer.java index 56a4f988..6e3aa06b 100644 --- a/src/main/java/org/qortal/transform/transaction/UpdateNameTransactionTransformer.java +++ b/src/main/java/org/qortal/transform/transaction/UpdateNameTransactionTransformer.java @@ -18,11 +18,11 @@ import com.google.common.primitives.Longs; public class UpdateNameTransactionTransformer extends TransactionTransformer { // Property lengths - private static final int OWNER_LENGTH = ADDRESS_LENGTH; private static final int NAME_SIZE_LENGTH = INT_LENGTH; - private static final int DATA_SIZE_LENGTH = INT_LENGTH; + private static final int NEW_NAME_SIZE_LENGTH = INT_LENGTH; + private static final int NEW_DATA_SIZE_LENGTH = INT_LENGTH; - private static final int EXTRAS_LENGTH = OWNER_LENGTH + NAME_SIZE_LENGTH + DATA_SIZE_LENGTH; + private static final int EXTRAS_LENGTH = NAME_SIZE_LENGTH + NEW_NAME_SIZE_LENGTH + NEW_DATA_SIZE_LENGTH; protected static final TransactionLayout layout; @@ -33,10 +33,11 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer { layout.add("transaction's groupID", TransformationType.INT); layout.add("reference", TransformationType.SIGNATURE); layout.add("name owner's public key", TransformationType.PUBLIC_KEY); - layout.add("name's new owner", TransformationType.ADDRESS); layout.add("name length", TransformationType.INT); layout.add("name", TransformationType.STRING); - layout.add("new data length", TransformationType.INT); + layout.add("new name's length (0 for no change)", TransformationType.INT); + layout.add("new name", TransformationType.STRING); + layout.add("new data length (0 for no change)", TransformationType.INT); layout.add("new data", TransformationType.STRING); layout.add("fee", TransformationType.AMOUNT); layout.add("signature", TransformationType.SIGNATURE); @@ -52,10 +53,10 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer { byte[] ownerPublicKey = Serialization.deserializePublicKey(byteBuffer); - String newOwner = Serialization.deserializeAddress(byteBuffer); - String name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE); + String newName = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE); + String newData = Serialization.deserializeSizedString(byteBuffer, Name.MAX_DATA_SIZE); long fee = byteBuffer.getLong(); @@ -65,13 +66,14 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer { BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, ownerPublicKey, fee, signature); - return new UpdateNameTransactionData(baseTransactionData, newOwner, name, newData); + return new UpdateNameTransactionData(baseTransactionData, name, newName, newData); } public static int getDataLength(TransactionData transactionData) throws TransformationException { UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData; return getBaseLength(transactionData) + EXTRAS_LENGTH + Utf8.encodedLength(updateNameTransactionData.getName()) + + Utf8.encodedLength(updateNameTransactionData.getNewName()) + Utf8.encodedLength(updateNameTransactionData.getNewData()); } @@ -83,10 +85,10 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer { transformCommonBytes(transactionData, bytes); - Serialization.serializeAddress(bytes, updateNameTransactionData.getNewOwner()); - Serialization.serializeSizedString(bytes, updateNameTransactionData.getName()); + Serialization.serializeSizedString(bytes, updateNameTransactionData.getNewName()); + Serialization.serializeSizedString(bytes, updateNameTransactionData.getNewData()); bytes.write(Longs.toByteArray(updateNameTransactionData.getFee())); diff --git a/src/main/java/org/qortal/utils/Unicode.java b/src/main/java/org/qortal/utils/Unicode.java new file mode 100644 index 00000000..8a9092ea --- /dev/null +++ b/src/main/java/org/qortal/utils/Unicode.java @@ -0,0 +1,220 @@ +package org.qortal.utils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.text.Normalizer; +import java.text.Normalizer.Form; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.TreeMap; + +import com.google.common.base.CharMatcher; + +import net.codebox.homoglyph.HomoglyphBuilder; + +public abstract class Unicode { + + public static final String NO_BREAK_SPACE = "\u00a0"; + + public static final String ZERO_WIDTH_SPACE = "\u200b"; + public static final String ZERO_WIDTH_NON_JOINER = "\u200c"; + public static final String ZERO_WIDTH_JOINER = "\u200d"; + public static final String WORD_JOINER = "\u2060"; + public static final String ZERO_WIDTH_NO_BREAK_SPACE = "\ufeff"; + + public static final CharMatcher ZERO_WIDTH_CHAR_MATCHER = CharMatcher.anyOf(ZERO_WIDTH_SPACE + ZERO_WIDTH_NON_JOINER + ZERO_WIDTH_JOINER + WORD_JOINER + ZERO_WIDTH_NO_BREAK_SPACE); + + private static int[] homoglyphCodePoints; + private static int[] reducedCodePoints; + + private static final String CHAR_CODES_FILE = "/char_codes.txt"; + + static { + buildHomoglyphCodePointArrays(); + } + + /** Returns string in Unicode canonical normalized form (NFC),
+ * with zero-width spaces/joiners removed,
+ * leading/trailing whitespace trimmed
+ * and all other whitespace blocks collapsed into a single space character. + *

+ * Example: [ZWS] means zero-width space + *

    + *
  • " powdered [TAB] to[ZWS]ast " becomes "powdered toast"
  • + *
+ *

+ * @see Form#NFKC + * @see Unicode#removeZeroWidth(String) + * @see CharMatcher#whitespace() + * @see CharMatcher#trimAndCollapseFrom(CharSequence, char) + */ + public static String normalize(String input) { + String output; + + // Normalize + output = Normalizer.normalize(input, Form.NFKC); + + // Remove zero-width code-points, used for rendering + output = removeZeroWidth(output); + + // Normalize whitespace + output = CharMatcher.whitespace().trimAndCollapseFrom(output, ' '); + + return output; + } + + /** Returns string after normalization,
+ * conversion to lowercase (locale insensitive)
+ * and homoglyphs replaced with simpler, reduced codepoints. + *

+ * Example: + *

    + *
  • " TΟÁST " becomes "toast" + *
+ *

+ * @see Form#NFKC + * @see Unicode#removeZeroWidth(String) + * @see CharMatcher#whitespace() + * @see CharMatcher#trimAndCollapseFrom(CharSequence, char) + * @see String#toLowerCase(Locale) + * @see Locale#ROOT + * @see Unicode#reduceHomoglyphs(String) + */ + public static String sanitize(String input) { + String output; + + // Normalize + output = Normalizer.normalize(input, Form.NFKD); + + // Remove zero-width code-points, used for rendering + output = removeZeroWidth(output); + + // Normalize whitespace + output = CharMatcher.whitespace().trimAndCollapseFrom(output, ' '); + + // Remove accents, combining marks + output = output.replaceAll("[\\p{M}\\p{C}]", ""); + + // Convert to lowercase + output = output.toLowerCase(Locale.ROOT); + + // Reduce homoglyphs + output = reduceHomoglyphs(output); + + return output; + } + + public static String removeZeroWidth(String input) { + return ZERO_WIDTH_CHAR_MATCHER.removeFrom(input); + } + + public static String reduceHomoglyphs(String input) { + CodePoints codePoints = new CodePoints(input); + final int length = codePoints.getLength(); + + for (int i = 0; i < length; ++i) { + int inputCodePoint = codePoints.getValue(i); + + int index = Arrays.binarySearch(homoglyphCodePoints, inputCodePoint); + if (index >= 0) + codePoints.setValue(i, reducedCodePoints[index]); + } + + return codePoints.toString(); + } + + private static void buildHomoglyphCodePointArrays() { + final InputStream is = HomoglyphBuilder.class.getResourceAsStream(CHAR_CODES_FILE); + + if (is == null) + throw new MissingResourceException("Unable to read " + CHAR_CODES_FILE, HomoglyphBuilder.class.getName(), + CHAR_CODES_FILE); + + final Reader reader = new InputStreamReader(is); + + Map homoglyphReductions = new TreeMap<>(); + + try (final BufferedReader bufferedReader = new BufferedReader(reader)) { + String line; + + while ((line = bufferedReader.readLine()) != null) { + line = line.trim(); + + if (line.startsWith("#") || line.length() == 0) + continue; + + String[] charCodes = line.split(","); + + // We consider the first charCode to be the 'reduced' form + int reducedCodepoint; + try { + reducedCodepoint = Integer.parseInt(charCodes[0], 16); + } catch (NumberFormatException ex) { + // ignore badly formatted lines + continue; + } + + // Map remaining charCodes + for (int i = 1; i < charCodes.length; ++i) + try { + int homoglyphCodepoint = Integer.parseInt(charCodes[i], 16); + + homoglyphReductions.put(homoglyphCodepoint, reducedCodepoint); + } catch (NumberFormatException ex) { + // ignore + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + homoglyphCodePoints = homoglyphReductions.keySet().stream().mapToInt(i -> i).toArray(); + reducedCodePoints = homoglyphReductions.values().stream().mapToInt(i -> i).toArray(); + } + + private static class CodePoints { + private final int[] codepointArray; + + public CodePoints(String text) { + final List codepointList = new ArrayList<>(); + + int codepoint; + for (int offset = 0; offset < text.length(); offset += Character.charCount(codepoint)) { + codepoint = text.codePointAt(offset); + codepointList.add(codepoint); + } + + this.codepointArray = codepointList.stream().mapToInt(i -> i).toArray(); + } + + public int getValue(int i) { + return codepointArray[i]; + } + + public void setValue(int i, int codepoint) { + codepointArray[i] = codepoint; + } + + public int getLength() { + return codepointArray.length; + } + + public String toString() { + final StringBuilder sb = new StringBuilder(this.codepointArray.length); + + for (int i = 0; i < this.codepointArray.length; i++) + sb.appendCodePoint(this.codepointArray[i]); + + return sb.toString(); + } + } + +} diff --git a/src/test/java/org/qortal/test/UnicodeTests.java b/src/test/java/org/qortal/test/UnicodeTests.java new file mode 100644 index 00000000..2e0f7968 --- /dev/null +++ b/src/test/java/org/qortal/test/UnicodeTests.java @@ -0,0 +1,38 @@ +package org.qortal.test; + +import static org.junit.Assert.*; +import static org.qortal.utils.Unicode.*; + +import org.junit.Test; +import org.qortal.utils.Unicode; + +public class UnicodeTests { + + @Test + public void testWhitespace() { + String input = " " + NO_BREAK_SPACE + "test "; + + String output = Unicode.normalize(input); + + assertEquals("trim & collapse failed", "test", output); + } + + @Test + public void testCaseComparison() { + String input1 = " " + NO_BREAK_SPACE + "test "; + String input2 = " " + NO_BREAK_SPACE + "TEST " + ZERO_WIDTH_SPACE; + + assertEquals("strings should match", Unicode.sanitize(input1), Unicode.sanitize(input2)); + } + + @Test + public void testHomoglyph() { + String omicron = "\u03bf"; + + String input1 = " " + NO_BREAK_SPACE + "toÁst "; + String input2 = " " + NO_BREAK_SPACE + "t" + omicron + "ast " + ZERO_WIDTH_SPACE; + + assertEquals("strings should match", Unicode.sanitize(input1), Unicode.sanitize(input2)); + } + +} diff --git a/src/test/java/org/qortal/test/api/NamesApiTests.java b/src/test/java/org/qortal/test/api/NamesApiTests.java index ae7248b4..2d31c8c2 100644 --- a/src/test/java/org/qortal/test/api/NamesApiTests.java +++ b/src/test/java/org/qortal/test/api/NamesApiTests.java @@ -46,7 +46,7 @@ public class NamesApiTests extends ApiCommon { PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String name = "test-name"; - RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); TransactionUtils.signAndMint(repository, transactionData, alice); assertNotNull(this.namesResource.getNamesByAddress(alice.getAddress(), null, null, null)); @@ -61,7 +61,7 @@ public class NamesApiTests extends ApiCommon { PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String name = "test-name"; - RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); TransactionUtils.signAndMint(repository, transactionData, alice); assertNotNull(this.namesResource.getName(name)); @@ -76,7 +76,7 @@ public class NamesApiTests extends ApiCommon { String name = "test-name"; long price = 1_23456789L; - TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); TransactionUtils.signAndMint(repository, transactionData, alice); // Sell-name diff --git a/src/test/java/org/qortal/test/common/transaction/RegisterNameTestTransaction.java b/src/test/java/org/qortal/test/common/transaction/RegisterNameTestTransaction.java index 14f93657..0450fbee 100644 --- a/src/test/java/org/qortal/test/common/transaction/RegisterNameTestTransaction.java +++ b/src/test/java/org/qortal/test/common/transaction/RegisterNameTestTransaction.java @@ -9,14 +9,13 @@ import org.qortal.repository.Repository; public class RegisterNameTestTransaction extends TestTransaction { public static TransactionData randomTransaction(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException { - String owner = account.getAddress(); String name = "test name"; if (!wantValid) name += " " + random.nextInt(1_000_000); String data = "{ \"key\": \"value\" }"; - return new RegisterNameTransactionData(generateBase(account), owner, name, data); + return new RegisterNameTransactionData(generateBase(account), name, data); } } diff --git a/src/test/java/org/qortal/test/naming/BuySellTests.java b/src/test/java/org/qortal/test/naming/BuySellTests.java index 0d323c5b..f0320da5 100644 --- a/src/test/java/org/qortal/test/naming/BuySellTests.java +++ b/src/test/java/org/qortal/test/naming/BuySellTests.java @@ -61,7 +61,7 @@ public class BuySellTests extends Common { @Test public void testRegisterName() throws DataException { // Register-name - RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); TransactionUtils.signAndMint(repository, transactionData, alice); String name = transactionData.getName(); @@ -95,7 +95,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", price, nameData.getSalePrice()); // Orphan sell-name @@ -103,7 +103,7 @@ public class BuySellTests extends Common { // Check name no longer for sale nameData = repository.getNameRepository().fromName(name); - assertFalse(nameData.getIsForSale()); + assertFalse(nameData.isForSale()); // Not concerned about price // Re-process sell-name @@ -111,7 +111,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", price, nameData.getSalePrice()); // Orphan sell-name and register-name @@ -133,7 +133,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", price, nameData.getSalePrice()); } @@ -150,7 +150,7 @@ public class BuySellTests extends Common { // Check name is no longer for sale nameData = repository.getNameRepository().fromName(name); - assertFalse(nameData.getIsForSale()); + assertFalse(nameData.isForSale()); // Not concerned about price // Orphan cancel sell-name @@ -158,7 +158,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", price, nameData.getSalePrice()); } @@ -177,7 +177,7 @@ public class BuySellTests extends Common { // Check name is sold nameData = repository.getNameRepository().fromName(name); - assertFalse(nameData.getIsForSale()); + assertFalse(nameData.isForSale()); // Not concerned about price // Orphan buy-name @@ -185,7 +185,7 @@ public class BuySellTests extends Common { // Check name is for sale (not sold) nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", price, nameData.getSalePrice()); // Re-process buy-name @@ -193,7 +193,7 @@ public class BuySellTests extends Common { // Check name is sold nameData = repository.getNameRepository().fromName(name); - assertFalse(nameData.getIsForSale()); + assertFalse(nameData.isForSale()); // Not concerned about price assertEquals(bob.getAddress(), nameData.getOwner()); @@ -202,7 +202,7 @@ public class BuySellTests extends Common { // Check name no longer for sale nameData = repository.getNameRepository().fromName(name); - assertFalse(nameData.getIsForSale()); + assertFalse(nameData.isForSale()); // Not concerned about price assertEquals(alice.getAddress(), nameData.getOwner()); @@ -214,7 +214,7 @@ public class BuySellTests extends Common { // Check name is sold nameData = repository.getNameRepository().fromName(name); - assertFalse(nameData.getIsForSale()); + assertFalse(nameData.isForSale()); // Not concerned about price assertEquals(bob.getAddress(), nameData.getOwner()); } @@ -233,7 +233,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", newPrice, nameData.getSalePrice()); // Orphan sell-name @@ -241,7 +241,7 @@ public class BuySellTests extends Common { // Check name no longer for sale nameData = repository.getNameRepository().fromName(name); - assertFalse(nameData.getIsForSale()); + assertFalse(nameData.isForSale()); // Not concerned about price // Re-process sell-name @@ -249,7 +249,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", newPrice, nameData.getSalePrice()); // Orphan sell-name and buy-name @@ -257,7 +257,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); // Note: original sale price assertEquals("price incorrect", price, nameData.getSalePrice()); assertEquals(alice.getAddress(), nameData.getOwner()); @@ -273,7 +273,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", newPrice, nameData.getSalePrice()); assertEquals(bob.getAddress(), nameData.getOwner()); } diff --git a/src/test/java/org/qortal/test/naming/MiscTests.java b/src/test/java/org/qortal/test/naming/MiscTests.java index d9cbf6fc..0bb750d0 100644 --- a/src/test/java/org/qortal/test/naming/MiscTests.java +++ b/src/test/java/org/qortal/test/naming/MiscTests.java @@ -8,12 +8,16 @@ import org.junit.Before; import org.junit.Test; import org.qortal.account.PrivateKeyAccount; import org.qortal.data.transaction.RegisterNameTransactionData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.data.transaction.UpdateNameTransactionData; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; import org.qortal.test.common.Common; import org.qortal.test.common.TransactionUtils; import org.qortal.test.common.transaction.TestTransaction; +import org.qortal.transaction.Transaction; +import org.qortal.transaction.Transaction.ValidationResult; public class MiscTests extends Common { @@ -27,9 +31,10 @@ public class MiscTests extends Common { try (final Repository repository = RepositoryManager.getRepository()) { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); - String name = "test-name"; + String name = "initial-name"; + String data = "initial-data"; - RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); TransactionUtils.signAndMint(repository, transactionData, alice); List recentNames = repository.getNameRepository().getRecentNames(0L); @@ -39,4 +44,56 @@ public class MiscTests extends Common { } } + // test trying to register same name twice + @Test + public void testDuplicateRegisterName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + String data = "{}"; + + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // duplicate + String duplicateName = "TEST-nÁme"; + transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), duplicateName, data); + Transaction transaction = Transaction.fromData(repository, transactionData); + transaction.sign(alice); + + ValidationResult result = transaction.importAsUnconfirmed(); + assertTrue("Transaction should be invalid", ValidationResult.OK != result); + } + } + + // test register then trying to update another name to existing name + @Test + public void testUpdateToExistingName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + String data = "{}"; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Register another name that we will later attempt to rename to first name (above) + String otherName = "new-name"; + String otherData = ""; + transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), otherName, otherData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // we shouldn't be able to update name to existing name + String duplicateName = "TEST-nÁme"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), otherName, duplicateName, otherData); + Transaction transaction = Transaction.fromData(repository, transactionData); + transaction.sign(alice); + + ValidationResult result = transaction.importAsUnconfirmed(); + assertTrue("Transaction should be invalid", ValidationResult.OK != result); + } + } + } diff --git a/src/test/java/org/qortal/test/naming/UpdateTests.java b/src/test/java/org/qortal/test/naming/UpdateTests.java new file mode 100644 index 00000000..ffbf7177 --- /dev/null +++ b/src/test/java/org/qortal/test/naming/UpdateTests.java @@ -0,0 +1,334 @@ +package org.qortal.test.naming; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.data.transaction.RegisterNameTransactionData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.data.transaction.UpdateNameTransactionData; +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.TransactionUtils; +import org.qortal.test.common.transaction.TestTransaction; + +public class UpdateTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @Test + public void testUpdateName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "initial-name"; + String initialData = "initial-data"; + + TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); + TransactionUtils.signAndMint(repository, initialTransactionData, alice); + + String newName = "new-name"; + String newData = ""; + TransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newName, newData); + TransactionUtils.signAndMint(repository, updateTransactionData, alice); + + // Check old name no longer exists + assertFalse(repository.getNameRepository().nameExists(initialName)); + + // Check new name exists + assertTrue(repository.getNameRepository().nameExists(newName)); + + // Check updated timestamp is correct + assertEquals((Long) updateTransactionData.getTimestamp(), repository.getNameRepository().fromName(newName).getUpdated()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check new name no longer exists + assertFalse(repository.getNameRepository().nameExists(newName)); + + // Check old name exists again + assertTrue(repository.getNameRepository().nameExists(initialName)); + + // Check updated timestamp is empty + assertNull(repository.getNameRepository().fromName(initialName).getUpdated()); + } + } + + @Test + public void testUpdateNameSameOwner() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "initial-name"; + String initialData = "initial-data"; + + TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); + TransactionUtils.signAndMint(repository, initialTransactionData, alice); + + String newName = "Initial-Name"; + String newData = ""; + TransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newName, newData); + TransactionUtils.signAndMint(repository, updateTransactionData, alice); + + // Check old name no longer exists + assertFalse(repository.getNameRepository().nameExists(initialName)); + + // Check new name exists + assertTrue(repository.getNameRepository().nameExists(newName)); + + // Check updated timestamp is correct + assertEquals((Long) updateTransactionData.getTimestamp(), repository.getNameRepository().fromName(newName).getUpdated()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check new name no longer exists + assertFalse(repository.getNameRepository().nameExists(newName)); + + // Check old name exists again + assertTrue(repository.getNameRepository().nameExists(initialName)); + + // Check updated timestamp is empty + assertNull(repository.getNameRepository().fromName(initialName).getUpdated()); + } + } + + // Test that reverting using previous UPDATE_NAME works as expected + @Test + public void testDoubleUpdateName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "initial-name"; + String initialData = "initial-data"; + + TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); + TransactionUtils.signAndMint(repository, initialTransactionData, alice); + + String middleName = "middle-name"; + String middleData = ""; + TransactionData middleTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData); + TransactionUtils.signAndMint(repository, middleTransactionData, alice); + + // Check old name no longer exists + assertFalse(repository.getNameRepository().nameExists(initialName)); + + // Check new name exists + assertTrue(repository.getNameRepository().nameExists(middleName)); + + String newestName = "newest-name"; + String newestData = "newest-data"; + TransactionData newestTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), middleName, newestName, newestData); + TransactionUtils.signAndMint(repository, newestTransactionData, alice); + + // Check previous name no longer exists + assertFalse(repository.getNameRepository().nameExists(middleName)); + + // Check newest name exists + assertTrue(repository.getNameRepository().nameExists(newestName)); + + // Check updated timestamp is correct + assertEquals((Long) newestTransactionData.getTimestamp(), repository.getNameRepository().fromName(newestName).getUpdated()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check newest name no longer exists + assertFalse(repository.getNameRepository().nameExists(newestName)); + + // Check previous name exists again + assertTrue(repository.getNameRepository().nameExists(middleName)); + + // Check updated timestamp is correct + assertEquals((Long) middleTransactionData.getTimestamp(), repository.getNameRepository().fromName(middleName).getUpdated()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check new name no longer exists + assertFalse(repository.getNameRepository().nameExists(middleName)); + + // Check original name exists again + assertTrue(repository.getNameRepository().nameExists(initialName)); + + // Check updated timestamp is empty + assertNull(repository.getNameRepository().fromName(initialName).getUpdated()); + } + } + + // Test that reverting using previous UPDATE_NAME works as expected + @Test + public void testIntermediateUpdateName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "initial-name"; + String initialData = "initial-data"; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Don't update name, but update data. + // This tests whether reverting a future update/sale can find the correct previous name + String middleName = ""; + String middleData = "middle-data"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check old name still exists + assertTrue(repository.getNameRepository().nameExists(initialName)); + + String newestName = "newest-name"; + String newestData = "newest-data"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newestName, newestData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check previous name no longer exists + assertFalse(repository.getNameRepository().nameExists(initialName)); + + // Check newest name exists + assertTrue(repository.getNameRepository().nameExists(newestName)); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check original name exists again + assertTrue(repository.getNameRepository().nameExists(initialName)); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check original name still exists + assertTrue(repository.getNameRepository().nameExists(initialName)); + } + } + + @Test + public void testUpdateData() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "initial-name"; + String initialData = "initial-data"; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + String newName = ""; + String newData = "new-data"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newName, newData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check name still exists + assertTrue(repository.getNameRepository().nameExists(initialName)); + + // Check data is correct + assertEquals(newData, repository.getNameRepository().fromName(initialName).getData()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check name still exists + assertTrue(repository.getNameRepository().nameExists(initialName)); + + // Check old data restored + assertEquals(initialData, repository.getNameRepository().fromName(initialName).getData()); + } + } + + // Test that reverting using previous UPDATE_NAME works as expected + @Test + public void testDoubleUpdateData() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "initial-name"; + String initialData = "initial-data"; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Update data + String middleName = "middle-name"; + String middleData = "middle-data"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check data is correct + assertEquals(middleData, repository.getNameRepository().fromName(middleName).getData()); + + String newestName = "newest-name"; + String newestData = "newest-data"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), middleName, newestName, newestData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check data is correct + assertEquals(newestData, repository.getNameRepository().fromName(newestName).getData()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check data is correct + assertEquals(middleData, repository.getNameRepository().fromName(middleName).getData()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check data is correct + assertEquals(initialData, repository.getNameRepository().fromName(initialName).getData()); + } + } + + // Test that reverting using previous UPDATE_NAME works as expected + @Test + public void testIntermediateUpdateData() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "initial-name"; + String initialData = "initial-data"; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Don't update data, but update name. + // This tests whether reverting a future update/sale can find the correct previous data + String middleName = "middle-name"; + String middleData = ""; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check data is correct + assertEquals(initialData, repository.getNameRepository().fromName(middleName).getData()); + + String newestName = "newest-name"; + String newestData = "newest-data"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), middleName, newestName, newestData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check data is correct + assertEquals(newestData, repository.getNameRepository().fromName(newestName).getData()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check data is correct + assertEquals(initialData, repository.getNameRepository().fromName(middleName).getData()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check data is correct + assertEquals(initialData, repository.getNameRepository().fromName(initialName).getData()); + } + } + +}