Major work on Registered Names

Changes include:

* Allowing renaming
* Tracking last-updated timestamps
* More stringent Unicode processing
* Way more unit tests
* Max name length reduction to 40 chars

Note: HSQLDB repository table changes
This commit is contained in:
catbref 2020-05-15 14:08:46 +01:00
parent cea0cee9a8
commit 197c742ce7
20 changed files with 982 additions and 305 deletions

View File

@ -504,6 +504,12 @@
<artifactId>mail</artifactId>
<version>1.5.0-b01</version>
</dependency>
<!-- Unicode homoglyph utilities -->
<dependency>
<groupId>net.codebox</groupId>
<artifactId>homoglyph</artifactId>
<version>1.2.0</version>
</dependency>
<!-- Jetty -->
<dependency>
<groupId>org.eclipse.jetty</groupId>

View File

@ -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,20 +62,13 @@ 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 getOwner() {
return this.owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getName() {
return this.name;
}
@ -70,6 +77,22 @@ public class NameData {
this.name = name;
}
public String getReducedName() {
return this.reducedName;
}
public void setReducedName(String reducedName) {
this.reducedName = reducedName;
}
public String getOwner() {
return this.owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getData() {
return this.data;
}
@ -90,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;
}
@ -114,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;
}

View File

@ -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,13 +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 = "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
@ -36,12 +46,18 @@ public class RegisterNameTransactionData extends TransactionData {
}
/** From repository */
public RegisterNameTransactionData(BaseTransactionData baseTransactionData, String name, String data) {
public RegisterNameTransactionData(BaseTransactionData baseTransactionData, String name, String data, String reducedName) {
super(TransactionType.REGISTER_NAME, baseTransactionData);
this.registrantPublicKey = baseTransactionData.creatorPublicKey;
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
@ -58,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;
}
}

View File

@ -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 = "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 name, String newName, 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.name = name;
this.newName = newName;
this.newData = newData;
this.reducedNewName = reducedNewName;
this.nameReference = nameReference;
}
/** From network/API */
public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String name, String newName, String newData) {
this(baseTransactionData, name, newName, newData, null);
this(baseTransactionData, name, newName, newData, Name.reduceName(newName), null);
}
// Getters / setters
@ -73,6 +85,14 @@ public class UpdateNameTransactionData extends TransactionData {
return this.newData;
}
public String getReducedNewName() {
return this.reducedNewName;
}
public void setReducedNewName(String reducedNewName) {
this.reducedNewName = reducedNewName;
}
public byte[] getNameReference() {
return this.nameReference;
}

View File

@ -13,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 {
@ -22,7 +24,7 @@ public class Name {
// Useful constants
public static final int MIN_NAME_SIZE = 3;
public static final int MAX_NAME_SIZE = 400;
public static final int MAX_NAME_SIZE = 40;
public static final int MAX_DATA_SIZE = 4000;
// Constructors
@ -37,9 +39,10 @@ public class Name {
this.repository = repository;
String owner = Crypto.toAddress(registerNameTransactionData.getRegistrantPublicKey());
String reducedName = Unicode.sanitize(registerNameTransactionData.getName());
this.nameData = new NameData(owner,
registerNameTransactionData.getName(), registerNameTransactionData.getData(), registerNameTransactionData.getTimestamp(),
this.nameData = new NameData(registerNameTransactionData.getName(), reducedName, owner,
registerNameTransactionData.getData(), registerNameTransactionData.getTimestamp(),
registerNameTransactionData.getSignature(), registerNameTransactionData.getTxGroupId());
}
@ -57,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);
}
@ -65,55 +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");
String previousName = this.nameData.getName();
switch (previousTransactionData.getType()) {
case REGISTER_NAME: {
RegisterNameTransactionData previousRegisterNameTransactionData = (RegisterNameTransactionData) previousTransactionData;
this.nameData.setName(previousRegisterNameTransactionData.getName());
this.nameData.setData(previousRegisterNameTransactionData.getData());
break;
}
case UPDATE_NAME: {
UpdateNameTransactionData previousUpdateNameTransactionData = (UpdateNameTransactionData) previousTransactionData;
if (!previousUpdateNameTransactionData.getNewName().isBlank())
this.nameData.setName(previousUpdateNameTransactionData.getNewName());
if (!previousUpdateNameTransactionData.getNewData().isEmpty())
this.nameData.setData(previousUpdateNameTransactionData.getNewData());
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");
}
this.repository.getNameRepository().save(this.nameData);
if (!this.nameData.getName().equals(previousName))
// Name has changed, delete old entry
this.repository.getNameRepository().delete(previousName);
}
public void update(UpdateNameTransactionData updateNameTransactionData) throws DataException {
// Update reference in transaction data
updateNameTransactionData.setNameReference(this.nameData.getReference());
@ -121,12 +79,15 @@ public class Name {
// New name reference is this transaction's signature
this.nameData.setReference(updateNameTransactionData.getSignature());
// Set name's last-updated timestamp
this.nameData.setUpdated(updateNameTransactionData.getTimestamp());
// Update name and data where appropriate
if (!updateNameTransactionData.getNewName().isEmpty()) {
// If we're changing the name, we need to delete old entry
this.repository.getNameRepository().delete(nameData.getName());
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())
@ -138,15 +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));
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);
@ -182,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);
@ -195,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);
}
@ -210,22 +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());
// 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();
}
}

View File

@ -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<NameData> getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<NameData> getAllNames() throws DataException {

View File

@ -285,20 +285,22 @@ 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)");
// Register Name Transactions
stmt.execute("CREATE TABLE RegisterNameTransactions (signature Signature, registrant QortalPublicKey NOT NULL, name RegisteredName 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_name RegisteredName 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, "

View File

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

View File

@ -17,16 +17,17 @@ public class HSQLDBRegisterNameTransactionRepository extends HSQLDBTransactionRe
}
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
String sql = "SELECT 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 name = resultSet.getString(1);
String data = resultSet.getString(2);
String reducedName = resultSet.getString(2);
String data = resultSet.getString(3);
return new RegisterNameTransactionData(baseTransactionData, name, data);
return new RegisterNameTransactionData(baseTransactionData, name, data, reducedName);
} catch (SQLException e) {
throw new DataException("Unable to fetch register name transaction from repository", e);
}
@ -39,7 +40,8 @@ public class HSQLDBRegisterNameTransactionRepository extends HSQLDBTransactionRe
HSQLDBSaver saveHelper = new HSQLDBSaver("RegisterNameTransactions");
saveHelper.bind("signature", registerNameTransactionData.getSignature()).bind("registrant", registerNameTransactionData.getRegistrantPublicKey())
.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);

View File

@ -17,7 +17,7 @@ public class HSQLDBUpdateNameTransactionRepository extends HSQLDBTransactionRepo
}
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
String sql = "SELECT name, new_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)
@ -26,9 +26,10 @@ public class HSQLDBUpdateNameTransactionRepository extends HSQLDBTransactionRepo
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, name, newName, 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);
}
@ -42,7 +43,8 @@ public class HSQLDBUpdateNameTransactionRepository extends HSQLDBTransactionRepo
saveHelper.bind("signature", updateNameTransactionData.getSignature()).bind("owner", updateNameTransactionData.getOwnerPublicKey())
.bind("name", updateNameTransactionData.getName()).bind("new_name", updateNameTransactionData.getNewName())
.bind("new_data", updateNameTransactionData.getNewData()).bind("name_reference", updateNameTransactionData.getNameReference());
.bind("new_data", updateNameTransactionData.getNewData()).bind("reduced_new_name", updateNameTransactionData.getReducedNewName())
.bind("name_reference", updateNameTransactionData.getNameReference());
try {
saveHelper.execute(this.repository);

View File

@ -69,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

View File

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

View File

@ -11,6 +11,7 @@ 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;
@ -40,6 +41,15 @@ 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
@ -57,21 +67,24 @@ public class RegisterNameTransaction extends Transaction {
if (dataLength > 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;
// 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;
// If accounts are only allowed one registered name then check for this

View File

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

View File

@ -11,6 +11,7 @@ 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;
@ -40,6 +41,15 @@ public class UpdateNameTransaction extends Transaction {
return this.getCreator();
}
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
@Override
@ -51,8 +61,8 @@ public class UpdateNameTransaction extends Transaction {
if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE)
return ValidationResult.INVALID_NAME_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);
@ -73,8 +83,8 @@ public class UpdateNameTransaction extends Transaction {
if (newNameLength < Name.MIN_NAME_SIZE || newNameLength > Name.MAX_NAME_SIZE)
return ValidationResult.INVALID_NAME_LENGTH;
// Check new name is lowercase
if (!newName.equals(newName.toLowerCase()))
// Check new name is in normalized form (no leading/trailing whitespace, etc.)
if (!newName.equals(Unicode.normalize(newName)))
return ValidationResult.NAME_NOT_LOWER_CASE;
}
@ -89,6 +99,9 @@ public class UpdateNameTransaction extends Transaction {
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;
}
@ -101,7 +114,7 @@ public class UpdateNameTransaction 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;
Account owner = getOwner();
@ -110,8 +123,9 @@ public class UpdateNameTransaction extends Transaction {
if (!owner.getAddress().equals(nameData.getOwner()))
return ValidationResult.INVALID_NAME_OWNER;
// Check new name isn't already taken
if (this.repository.getNameRepository().nameExists(this.updateNameTransactionData.getNewName()))
// 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;
@ -129,7 +143,7 @@ public class UpdateNameTransaction extends Transaction {
@Override
public void orphan() throws DataException {
// Revert name
// Revert update
String nameToRevert = this.updateNameTransactionData.getNewName();
if (nameToRevert.isEmpty())

View File

@ -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),<br>
* with zero-width spaces/joiners removed,<br>
* leading/trailing whitespace trimmed<br>
* and all other whitespace blocks collapsed into a single space character.
* <p>
* Example: <tt><b>[ZWS]</b></tt> means zero-width space
* <ul>
* <li><tt>" powdered <b>[TAB]</b> to<b>[ZWS]</b>ast "</tt> becomes <tt>"powdered toast"</tt></li>
* </ul>
* <p>
* @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,<br>
* conversion to lowercase (locale insensitive)<br>
* and homoglyphs replaced with simpler, reduced codepoints.
* <p>
* Example:
* <ul>
* <li><tt>" T&Omicron;&Aacute;ST "</tt> becomes <tt>"toast"</tt>
* </ul>
* <p>
* @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<Integer, Integer> 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<Integer> 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();
}
}
}

View File

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

View File

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

View File

@ -13,7 +13,6 @@ 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;
@ -32,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), name, "{}");
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
TransactionUtils.signAndMint(repository, transactionData, alice);
List<String> recentNames = repository.getNameRepository().getRecentNames(0L);
@ -44,124 +44,6 @@ public class MiscTests extends Common {
}
}
@Test
public void testUpdateName() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
// Register-name
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String name = "test-name";
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
TransactionUtils.signAndMint(repository, transactionData, alice);
String newName = "new-name";
String newData = "";
transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, newName, newData);
TransactionUtils.signAndMint(repository, transactionData, alice);
// Check old name no longer exists
assertFalse(repository.getNameRepository().nameExists(name));
// Check new name exists
assertTrue(repository.getNameRepository().nameExists(newName));
// 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(name));
}
}
// 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 name = "test-name";
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
TransactionUtils.signAndMint(repository, transactionData, alice);
String newName = "new-name";
String newData = "";
transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, newName, newData);
TransactionUtils.signAndMint(repository, transactionData, alice);
// Check old name no longer exists
assertFalse(repository.getNameRepository().nameExists(name));
// Check new name exists
assertTrue(repository.getNameRepository().nameExists(newName));
String newestName = "newest-name";
String newestData = "abc";
transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), newName, newestName, newestData);
TransactionUtils.signAndMint(repository, transactionData, alice);
// Check previous name no longer exists
assertFalse(repository.getNameRepository().nameExists(newName));
// Check newest name exists
assertTrue(repository.getNameRepository().nameExists(newestName));
// 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(newName));
// orphan and recheck
BlockUtils.orphanLastBlock(repository);
// Check new name no longer exists
assertFalse(repository.getNameRepository().nameExists(newName));
// Check original name exists again
assertTrue(repository.getNameRepository().nameExists(name));
}
}
@Test
public void testUpdateData() 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);
String newName = "";
String newData = "new-data";
transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, newName, newData);
TransactionUtils.signAndMint(repository, transactionData, alice);
// Check name still exists
assertTrue(repository.getNameRepository().nameExists(name));
// Check data is correct
assertEquals(newData, repository.getNameRepository().fromName(name).getData());
// orphan and recheck
BlockUtils.orphanLastBlock(repository);
// Check name still exists
assertTrue(repository.getNameRepository().nameExists(name));
// Check old data restored
assertEquals(data, repository.getNameRepository().fromName(name).getData());
}
}
// test trying to register same name twice
@Test
public void testDuplicateRegisterName() throws DataException {
@ -169,12 +51,14 @@ public class MiscTests extends Common {
// Register-name
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String name = "test-name";
String data = "{}";
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
TransactionUtils.signAndMint(repository, transactionData, alice);
// duplicate
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
String duplicateName = "TEST-nÁme";
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), duplicateName, data);
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(alice);
@ -190,17 +74,20 @@ public class MiscTests extends Common {
// Register-name
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String name = "test-name";
String data = "{}";
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
TransactionUtils.signAndMint(repository, transactionData, alice);
String newName = "new-name";
String newData = "";
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), newName, newData);
// 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
transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), newName, name, newData);
String duplicateName = "TEST-nÁme";
transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), otherName, duplicateName, otherData);
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(alice);

View File

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