forked from Qortal/qortal
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:
parent
cea0cee9a8
commit
197c742ce7
6
pom.xml
6
pom.xml
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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, "
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
220
src/main/java/org/qortal/utils/Unicode.java
Normal file
220
src/main/java/org/qortal/utils/Unicode.java
Normal 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ΟÁ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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
38
src/test/java/org/qortal/test/UnicodeTests.java
Normal file
38
src/test/java/org/qortal/test/UnicodeTests.java
Normal 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));
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
334
src/test/java/org/qortal/test/naming/UpdateTests.java
Normal file
334
src/test/java/org/qortal/test/naming/UpdateTests.java
Normal 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user