Merge branch 'name-fixes' into launch

This commit is contained in:
catbref 2020-05-18 10:12:31 +01:00
commit 4baf442cb8
24 changed files with 1115 additions and 231 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,12 +62,29 @@ public class NameData {
this.creationGroupId = creationGroupId;
}
public NameData(String owner, String name, String data, long registered, byte[] reference, int creationGroupId) {
this(owner, name, data, registered, null, reference, false, null, creationGroupId);
// Typically used when registering a new name
public NameData(String name, String reducedName, String owner, String data, long registered, byte[] reference, int creationGroupId) {
this(name, reducedName, owner, data, registered, null, false, null, reference, creationGroupId);
}
// Getters / setters
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getReducedName() {
return this.reducedName;
}
public void setReducedName(String reducedName) {
this.reducedName = reducedName;
}
public String getOwner() {
return this.owner;
}
@ -62,10 +93,6 @@ public class NameData {
this.owner = owner;
}
public String getName() {
return this.name;
}
public String getData() {
return this.data;
}
@ -86,15 +113,7 @@ public class NameData {
this.updated = updated;
}
public byte[] getReference() {
return this.reference;
}
public void setReference(byte[] reference) {
this.reference = reference;
}
public boolean getIsForSale() {
public boolean isForSale() {
return this.isForSale;
}
@ -110,6 +129,14 @@ public class NameData {
this.salePrice = salePrice;
}
public byte[] getReference() {
return this.reference;
}
public void setReference(byte[] reference) {
this.reference = reference;
}
public int getCreationGroupId() {
return this.creationGroupId;
}

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,15 +19,21 @@ import io.swagger.v3.oas.annotations.media.Schema;
public class RegisterNameTransactionData extends TransactionData {
// Properties
@Schema(description = "registrant's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
private byte[] registrantPublicKey;
@Schema(description = "new owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v")
private String owner;
@Schema(description = "requested name", example = "my-name")
private String name;
@Schema(description = "simple name-related info in JSON format", example = "{ \"age\": 30 }")
private String data;
// For internal use
@XmlTransient
@Schema(hidden = true)
private String reducedName;
// Constructors
// For JAXB
@ -38,13 +46,18 @@ public class RegisterNameTransactionData extends TransactionData {
}
/** From repository */
public RegisterNameTransactionData(BaseTransactionData baseTransactionData, String owner, String name, String data) {
public RegisterNameTransactionData(BaseTransactionData baseTransactionData, String name, String data, String reducedName) {
super(TransactionType.REGISTER_NAME, baseTransactionData);
this.registrantPublicKey = baseTransactionData.creatorPublicKey;
this.owner = owner;
this.name = name;
this.data = data;
this.reducedName = reducedName;
}
/** From network */
public RegisterNameTransactionData(BaseTransactionData baseTransactionData, String name, String data) {
this(baseTransactionData, name, data, Name.reduceName(name));
}
// Getters / setters
@ -53,10 +66,6 @@ public class RegisterNameTransactionData extends TransactionData {
return this.registrantPublicKey;
}
public String getOwner() {
return this.owner;
}
public String getName() {
return this.name;
}
@ -65,4 +74,12 @@ public class RegisterNameTransactionData extends TransactionData {
return this.data;
}
public String getReducedName() {
return this.reducedName;
}
public void setReducedName(String reducedName) {
this.reducedName = reducedName;
}
}

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 = "new owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v")
private String newOwner;
@Schema(description = "which name to update", example = "my-name")
private String name;
@Schema(description = "new name", example = "my-new-name")
private String newName;
@Schema(description = "replacement simple name-related info in JSON format", example = "{ \"age\": 30 }")
private String newData;
// For internal use
@XmlTransient
@Schema(hidden = true)
private String reducedNewName;
// For internal use when orphaning
@XmlTransient
@Schema(hidden = true)
@ -40,19 +51,20 @@ public class UpdateNameTransactionData extends TransactionData {
}
/** From repository */
public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String newOwner, String name, String newData, byte[] nameReference) {
public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String name, String newName, String newData, String reducedNewName, byte[] nameReference) {
super(TransactionType.UPDATE_NAME, baseTransactionData);
this.ownerPublicKey = baseTransactionData.creatorPublicKey;
this.newOwner = newOwner;
this.name = name;
this.newName = newName;
this.newData = newData;
this.reducedNewName = reducedNewName;
this.nameReference = nameReference;
}
/** From network/API */
public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String newOwner, String name, String newData) {
this(baseTransactionData, newOwner, name, newData, null);
public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String name, String newName, String newData) {
this(baseTransactionData, name, newName, newData, Name.reduceName(newName), null);
}
// Getters / setters
@ -61,18 +73,26 @@ public class UpdateNameTransactionData extends TransactionData {
return this.ownerPublicKey;
}
public String getNewOwner() {
return this.newOwner;
}
public String getName() {
return this.name;
}
public String getNewName() {
return this.newName;
}
public String getNewData() {
return this.newData;
}
public String getReducedNewName() {
return this.reducedNewName;
}
public void setReducedNewName(String reducedNewName) {
this.reducedNewName = reducedNewName;
}
public byte[] getNameReference() {
return this.nameReference;
}

View File

@ -3,6 +3,7 @@ package org.qortal.naming;
import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.crypto.Crypto;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.BuyNameTransactionData;
import org.qortal.data.transaction.CancelSellNameTransactionData;
@ -12,6 +13,8 @@ import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.UpdateNameTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.utils.Unicode;
public class Name {
@ -20,7 +23,8 @@ public class Name {
private NameData nameData;
// Useful constants
public static final int MAX_NAME_SIZE = 400;
public static final int MIN_NAME_SIZE = 3;
public static final int MAX_NAME_SIZE = 40;
public static final int MAX_DATA_SIZE = 4000;
// Constructors
@ -33,8 +37,12 @@ public class Name {
*/
public Name(Repository repository, RegisterNameTransactionData registerNameTransactionData) {
this.repository = repository;
this.nameData = new NameData(registerNameTransactionData.getOwner(),
registerNameTransactionData.getName(), registerNameTransactionData.getData(), registerNameTransactionData.getTimestamp(),
String owner = Crypto.toAddress(registerNameTransactionData.getRegistrantPublicKey());
String reducedName = Unicode.sanitize(registerNameTransactionData.getName());
this.nameData = new NameData(registerNameTransactionData.getName(), reducedName, owner,
registerNameTransactionData.getData(), registerNameTransactionData.getTimestamp(),
registerNameTransactionData.getSignature(), registerNameTransactionData.getTxGroupId());
}
@ -52,6 +60,10 @@ public class Name {
// Processing
public static String reduceName(String name) {
return Unicode.sanitize(name);
}
public void register() throws DataException {
this.repository.getNameRepository().save(this.nameData);
}
@ -60,35 +72,6 @@ public class Name {
this.repository.getNameRepository().delete(this.nameData.getName());
}
private void revert() throws DataException {
TransactionData previousTransactionData = this.repository.getTransactionRepository().fromSignature(this.nameData.getReference());
if (previousTransactionData == null)
throw new DataException("Unable to revert name transaction as referenced transaction not found in repository");
switch (previousTransactionData.getType()) {
case REGISTER_NAME:
RegisterNameTransactionData previousRegisterNameTransactionData = (RegisterNameTransactionData) previousTransactionData;
this.nameData.setOwner(previousRegisterNameTransactionData.getOwner());
this.nameData.setData(previousRegisterNameTransactionData.getData());
break;
case UPDATE_NAME:
UpdateNameTransactionData previousUpdateNameTransactionData = (UpdateNameTransactionData) previousTransactionData;
this.nameData.setData(previousUpdateNameTransactionData.getNewData());
this.nameData.setOwner(previousUpdateNameTransactionData.getNewOwner());
break;
case BUY_NAME:
BuyNameTransactionData previousBuyNameTransactionData = (BuyNameTransactionData) previousTransactionData;
Account buyer = new PublicKeyAccount(this.repository, previousBuyNameTransactionData.getBuyerPublicKey());
this.nameData.setOwner(buyer.getAddress());
break;
default:
throw new IllegalStateException("Unable to revert name transaction due to unsupported referenced transaction");
}
}
public void update(UpdateNameTransactionData updateNameTransactionData) throws DataException {
// Update reference in transaction data
updateNameTransactionData.setNameReference(this.nameData.getReference());
@ -96,8 +79,18 @@ public class Name {
// New name reference is this transaction's signature
this.nameData.setReference(updateNameTransactionData.getSignature());
// Update Name's owner and data
this.nameData.setOwner(updateNameTransactionData.getNewOwner());
// Set name's last-updated timestamp
this.nameData.setUpdated(updateNameTransactionData.getTimestamp());
// Update name and data where appropriate
if (!updateNameTransactionData.getNewName().isEmpty()) {
this.nameData.setName(updateNameTransactionData.getNewName());
// If we're changing the name, we need to delete old entry
this.repository.getNameRepository().delete(updateNameTransactionData.getName());
}
if (!updateNameTransactionData.getNewData().isEmpty())
this.nameData.setData(updateNameTransactionData.getNewData());
// Save updated name data
@ -106,18 +99,68 @@ public class Name {
public void revert(UpdateNameTransactionData updateNameTransactionData) throws DataException {
// Previous name reference is taken from this transaction's cached copy
this.nameData.setReference(updateNameTransactionData.getNameReference());
byte[] nameReference = updateNameTransactionData.getNameReference();
// Previous Name's owner and/or data taken from referenced transaction
this.revert();
// Revert name's name-changing transaction reference
this.nameData.setReference(nameReference);
// Revert name's last-updated timestamp
this.nameData.setUpdated(fetchPreviousUpdateTimestamp(nameReference));
// We can find previous 'name' from update transaction
this.nameData.setName(updateNameTransactionData.getName());
// We might need to hunt for previous data value
if (!updateNameTransactionData.getNewData().isEmpty())
this.nameData.setData(findPreviousData(nameReference));
// Save reverted name data
this.repository.getNameRepository().save(this.nameData);
if (!updateNameTransactionData.getNewName().isEmpty())
// Name has changed, delete old entry
this.repository.getNameRepository().delete(updateNameTransactionData.getNewName());
// Remove reference to previous name-changing transaction
updateNameTransactionData.setNameReference(null);
}
private String findPreviousData(byte[] nameReference) throws DataException {
// Follow back through name-references until we hit the data we need
while (true) {
TransactionData previousTransactionData = this.repository.getTransactionRepository().fromSignature(nameReference);
if (previousTransactionData == null)
throw new DataException("Unable to revert name transaction as referenced transaction not found in repository");
switch (previousTransactionData.getType()) {
case REGISTER_NAME: {
RegisterNameTransactionData previousRegisterNameTransactionData = (RegisterNameTransactionData) previousTransactionData;
return previousRegisterNameTransactionData.getData();
}
case UPDATE_NAME: {
UpdateNameTransactionData previousUpdateNameTransactionData = (UpdateNameTransactionData) previousTransactionData;
if (!previousUpdateNameTransactionData.getNewData().isEmpty())
return previousUpdateNameTransactionData.getNewData();
nameReference = previousUpdateNameTransactionData.getNameReference();
break;
}
case BUY_NAME: {
BuyNameTransactionData previousBuyNameTransactionData = (BuyNameTransactionData) previousTransactionData;
nameReference = previousBuyNameTransactionData.getNameReference();
break;
}
default:
throw new IllegalStateException("Unable to revert name transaction due to unsupported referenced transaction");
}
}
}
public void sell(SellNameTransactionData sellNameTransactionData) throws DataException {
// Mark as for-sale and set price
this.nameData.setIsForSale(true);
@ -153,6 +196,10 @@ public class Name {
}
public void buy(BuyNameTransactionData buyNameTransactionData) throws DataException {
// Save previous name-changing reference in this transaction's data
// Caller is expected to save
buyNameTransactionData.setNameReference(this.nameData.getReference());
// Mark not for-sale but leave price in case we want to orphan
this.nameData.setIsForSale(false);
@ -166,12 +213,12 @@ public class Name {
// Update buyer's balance
buyer.modifyAssetBalance(Asset.QORT, - buyNameTransactionData.getAmount());
// Update reference in transaction data
buyNameTransactionData.setNameReference(this.nameData.getReference());
// New name reference is this transaction's signature
// Set name-changing reference to this transaction
this.nameData.setReference(buyNameTransactionData.getSignature());
// Set name's last-updated timestamp
this.nameData.setUpdated(buyNameTransactionData.getTimestamp());
// Save updated name data
this.repository.getNameRepository().save(this.nameData);
}
@ -181,25 +228,41 @@ public class Name {
this.nameData.setIsForSale(true);
this.nameData.setSalePrice(buyNameTransactionData.getAmount());
// Previous name reference is taken from this transaction's cached copy
// Previous name-changing reference is taken from this transaction's cached copy
this.nameData.setReference(buyNameTransactionData.getNameReference());
// Remove reference in transaction data
buyNameTransactionData.setNameReference(null);
// Revert name's last-updated timestamp
this.nameData.setUpdated(fetchPreviousUpdateTimestamp(buyNameTransactionData.getNameReference()));
// Revert to previous owner
this.nameData.setOwner(buyNameTransactionData.getSeller());
// Save updated name data
this.repository.getNameRepository().save(this.nameData);
// Revert buyer's balance
Account buyer = new PublicKeyAccount(this.repository, buyNameTransactionData.getBuyerPublicKey());
buyer.modifyAssetBalance(Asset.QORT, buyNameTransactionData.getAmount());
// Previous Name's owner and/or data taken from referenced transaction
this.revert();
// Revert seller's balance
Account seller = new Account(this.repository, this.nameData.getOwner());
Account seller = new Account(this.repository, buyNameTransactionData.getSeller());
seller.modifyAssetBalance(Asset.QORT, - buyNameTransactionData.getAmount());
// Save reverted name data
this.repository.getNameRepository().save(this.nameData);
// Clean previous name-changing reference from this transaction's data
// Caller is expected to save
buyNameTransactionData.setNameReference(null);
}
private Long fetchPreviousUpdateTimestamp(byte[] nameReference) throws DataException {
TransactionData previousTransactionData = this.repository.getTransactionRepository().fromSignature(nameReference);
if (previousTransactionData == null)
throw new DataException("Unable to revert name transaction as referenced transaction not found in repository");
// If we've hit REGISTER_NAME then we've run out of updates
if (previousTransactionData.getType() == TransactionType.REGISTER_NAME)
return null;
return previousTransactionData.getTimestamp();
}
}

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,24 @@ public class HSQLDBDatabaseUpdates {
case 8:
// Name-related
stmt.execute("CREATE TABLE Names (name RegisteredName, owner QortalAddress NOT NULL, "
+ "registered_when EpochMillis NOT NULL, updated_when EpochMillis, creation_group_id GroupID NOT NULL DEFAULT 0, "
+ "reference Signature, is_for_sale BOOLEAN NOT NULL DEFAULT FALSE, sale_price QortalAmount, data NameData NOT NULL, "
stmt.execute("CREATE TABLE Names (name RegisteredName, reduced_name RegisteredName, owner QortalAddress NOT NULL, "
+ "registered_when EpochMillis NOT NULL, updated_when EpochMillis, "
+ "is_for_sale BOOLEAN NOT NULL DEFAULT FALSE, sale_price QortalAmount, data NameData NOT NULL, "
+ "reference Signature, creation_group_id GroupID NOT NULL DEFAULT 0, "
+ "PRIMARY KEY (name))");
// For finding names by owner
stmt.execute("CREATE INDEX NamesOwnerIndex ON Names (owner)");
// For finding names by 'reduced' form
stmt.execute("CREATE INDEX NamesReducedNameIndex ON Names (reduced_name)");
// Register Name Transactions
stmt.execute("CREATE TABLE RegisterNameTransactions (signature Signature, registrant QortalPublicKey NOT NULL, name RegisteredName NOT NULL, "
+ "owner QortalAddress NOT NULL, data NameData NOT NULL, " + TRANSACTION_KEYS + ")");
+ "data NameData NOT NULL, reduced_name RegisteredName NOT NULL, " + TRANSACTION_KEYS + ")");
// Update Name Transactions
stmt.execute("CREATE TABLE UpdateNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, "
+ "new_owner QortalAddress NOT NULL, new_data NameData NOT NULL, name_reference Signature, " + TRANSACTION_KEYS + ")");
+ "new_name RegisteredName NOT NULL, new_data NameData NOT NULL, reduced_new_name RegisteredName NOT NULL, "
+ "name_reference Signature, " + TRANSACTION_KEYS + ")");
// Sell Name Transactions
stmt.execute("CREATE TABLE SellNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, "

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,17 +17,17 @@ public class HSQLDBRegisterNameTransactionRepository extends HSQLDBTransactionRe
}
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
String sql = "SELECT owner, name, data FROM RegisterNameTransactions WHERE signature = ?";
String sql = "SELECT name, reduced_name, data FROM RegisterNameTransactions WHERE signature = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
if (resultSet == null)
return null;
String owner = resultSet.getString(1);
String name = resultSet.getString(2);
String name = resultSet.getString(1);
String reducedName = resultSet.getString(2);
String data = resultSet.getString(3);
return new RegisterNameTransactionData(baseTransactionData, owner, name, data);
return new RegisterNameTransactionData(baseTransactionData, name, data, reducedName);
} catch (SQLException e) {
throw new DataException("Unable to fetch register name transaction from repository", e);
}
@ -40,8 +40,8 @@ public class HSQLDBRegisterNameTransactionRepository extends HSQLDBTransactionRe
HSQLDBSaver saveHelper = new HSQLDBSaver("RegisterNameTransactions");
saveHelper.bind("signature", registerNameTransactionData.getSignature()).bind("registrant", registerNameTransactionData.getRegistrantPublicKey())
.bind("owner", registerNameTransactionData.getOwner()).bind("name", registerNameTransactionData.getName())
.bind("data", registerNameTransactionData.getData());
.bind("name", registerNameTransactionData.getName()).bind("data", registerNameTransactionData.getData())
.bind("reduced_name", registerNameTransactionData.getReducedName());
try {
saveHelper.execute(this.repository);

View File

@ -17,18 +17,19 @@ public class HSQLDBUpdateNameTransactionRepository extends HSQLDBTransactionRepo
}
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
String sql = "SELECT new_owner, name, new_data, name_reference FROM UpdateNameTransactions WHERE signature = ?";
String sql = "SELECT name, new_name, new_data, reduced_new_name, name_reference FROM UpdateNameTransactions WHERE signature = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
if (resultSet == null)
return null;
String newOwner = resultSet.getString(1);
String name = resultSet.getString(2);
String name = resultSet.getString(1);
String newName = resultSet.getString(2);
String newData = resultSet.getString(3);
byte[] nameReference = resultSet.getBytes(4);
String reducedNewName = resultSet.getString(4);
byte[] nameReference = resultSet.getBytes(5);
return new UpdateNameTransactionData(baseTransactionData, newOwner, name, newData, nameReference);
return new UpdateNameTransactionData(baseTransactionData, name, newName, newData, reducedNewName, nameReference);
} catch (SQLException e) {
throw new DataException("Unable to fetch update name transaction from repository", e);
}
@ -41,8 +42,9 @@ public class HSQLDBUpdateNameTransactionRepository extends HSQLDBTransactionRepo
HSQLDBSaver saveHelper = new HSQLDBSaver("UpdateNameTransactions");
saveHelper.bind("signature", updateNameTransactionData.getSignature()).bind("owner", updateNameTransactionData.getOwnerPublicKey())
.bind("new_owner", updateNameTransactionData.getNewOwner()).bind("name", updateNameTransactionData.getName())
.bind("new_data", updateNameTransactionData.getNewData()).bind("name_reference", updateNameTransactionData.getNameReference());
.bind("name", updateNameTransactionData.getName()).bind("new_name", updateNameTransactionData.getNewName())
.bind("new_data", updateNameTransactionData.getNewData()).bind("reduced_new_name", updateNameTransactionData.getReducedNewName())
.bind("name_reference", updateNameTransactionData.getNameReference());
try {
saveHelper.execute(this.repository);

View File

@ -5,6 +5,7 @@ import java.util.List;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.BuyNameTransactionData;
@ -54,7 +55,7 @@ public class BuyNameTransaction extends Transaction {
// Check name size bounds
int nameLength = Utf8.encodedLength(name);
if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE)
if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE)
return ValidationResult.INVALID_NAME_LENGTH;
// Check name is lowercase
@ -68,7 +69,7 @@ public class BuyNameTransaction extends Transaction {
return ValidationResult.NAME_DOES_NOT_EXIST;
// Check name is currently for sale
if (!nameData.getIsForSale())
if (!nameData.isForSale())
return ValidationResult.NAME_NOT_FOR_SALE;
// Check buyer isn't trying to buy own name
@ -76,6 +77,11 @@ public class BuyNameTransaction extends Transaction {
if (buyer.getAddress().equals(nameData.getOwner()))
return ValidationResult.BUYER_ALREADY_OWNER;
// If accounts are only allowed one registered name then check for this
if (BlockChain.getInstance().oneNamePerAccount()
&& !this.repository.getNameRepository().getNamesByOwner(buyer.getAddress()).isEmpty())
return ValidationResult.MULTIPLE_NAMES_FORBIDDEN;
// Check expected seller currently owns name
if (!this.buyNameTransactionData.getSeller().equals(nameData.getOwner()))
return ValidationResult.INVALID_SELLER;
@ -84,7 +90,7 @@ public class BuyNameTransaction extends Transaction {
if (this.buyNameTransactionData.getAmount() != nameData.getSalePrice())
return ValidationResult.INVALID_AMOUNT;
// Check issuer has enough funds
// Check buyer has enough funds
if (buyer.getConfirmedBalance(Asset.QORT) < this.buyNameTransactionData.getFee())
return ValidationResult.NO_BALANCE;
@ -93,21 +99,21 @@ public class BuyNameTransaction extends Transaction {
@Override
public void process() throws DataException {
// Update Name
// Buy Name
Name name = new Name(this.repository, this.buyNameTransactionData.getName());
name.buy(this.buyNameTransactionData);
// Save transaction with updated "name reference" pointing to previous transaction that updated name
// Save transaction with updated "name reference" pointing to previous transaction that changed name
this.repository.getTransactionRepository().save(this.buyNameTransactionData);
}
@Override
public void orphan() throws DataException {
// Revert name
// Un-buy name
Name name = new Name(this.repository, this.buyNameTransactionData.getName());
name.unbuy(this.buyNameTransactionData);
// Save this transaction, with removed "name reference"
// Save this transaction, with previous "name reference"
this.repository.getTransactionRepository().save(this.buyNameTransactionData);
}

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

@ -6,12 +6,12 @@ import java.util.List;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.RegisterNameTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.naming.Name;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Unicode;
import com.google.common.base.Utf8;
@ -32,7 +32,7 @@ public class RegisterNameTransaction extends Transaction {
@Override
public List<String> getRecipientAddresses() throws DataException {
return Collections.singletonList(this.registerNameTransactionData.getOwner());
return Collections.emptyList();
}
// Navigation
@ -41,47 +41,55 @@ public class RegisterNameTransaction extends Transaction {
return this.getCreator();
}
private synchronized String getReducedName() {
if (this.registerNameTransactionData.getReducedName() == null) {
String reducedName = Name.reduceName(this.registerNameTransactionData.getName());
this.registerNameTransactionData.setReducedName(reducedName);
}
return this.registerNameTransactionData.getReducedName();
}
// Processing
@Override
public ValidationResult isValid() throws DataException {
Account registrant = getRegistrant();
// Check owner address is valid
if (!Crypto.isValidAddress(this.registerNameTransactionData.getOwner()))
return ValidationResult.INVALID_ADDRESS;
String name = this.registerNameTransactionData.getName();
// Check name size bounds
int nameLength = Utf8.encodedLength(this.registerNameTransactionData.getName());
if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE)
int nameLength = Utf8.encodedLength(name);
if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE)
return ValidationResult.INVALID_NAME_LENGTH;
// Check data size bounds
int dataLength = Utf8.encodedLength(this.registerNameTransactionData.getData());
if (dataLength < 1 || dataLength > Name.MAX_DATA_SIZE)
if (dataLength > Name.MAX_DATA_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
// Check name is lowercase
if (!this.registerNameTransactionData.getName().equals(this.registerNameTransactionData.getName().toLowerCase()))
// Check name is in normalized form (no leading/trailing whitespace, etc.)
if (!name.equals(Unicode.normalize(name)))
return ValidationResult.NAME_NOT_LOWER_CASE;
// Check registrant has enough funds
if (registrant.getConfirmedBalance(Asset.QORT) < this.registerNameTransactionData.getFee())
return ValidationResult.NO_BALANCE;
// Fill in missing reduced name. Caller is likely to save this as next step.
getReducedName();
return ValidationResult.OK;
}
@Override
public ValidationResult isProcessable() throws DataException {
// Check the name isn't already taken
if (this.repository.getNameRepository().nameExists(this.registerNameTransactionData.getName()))
if (this.repository.getNameRepository().reducedNameExists(getReducedName()))
return ValidationResult.NAME_ALREADY_REGISTERED;
Account registrant = getRegistrant();
// If accounts are only allowed one registered name then check for this
if (BlockChain.getInstance().oneNamePerAccount() && !this.repository.getNameRepository().getNamesByOwner(registrant.getAddress()).isEmpty())
if (BlockChain.getInstance().oneNamePerAccount()
&& !this.repository.getNameRepository().getNamesByOwner(getRegistrant().getAddress()).isEmpty())
return ValidationResult.MULTIPLE_NAMES_FORBIDDEN;
return ValidationResult.OK;

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

@ -5,13 +5,13 @@ import java.util.List;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.crypto.Crypto;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.UpdateNameTransactionData;
import org.qortal.naming.Name;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Unicode;
import com.google.common.base.Utf8;
@ -32,7 +32,7 @@ public class UpdateNameTransaction extends Transaction {
@Override
public List<String> getRecipientAddresses() throws DataException {
return Collections.singletonList(this.updateNameTransactionData.getNewOwner());
return Collections.emptyList();
}
// Navigation
@ -41,8 +41,13 @@ public class UpdateNameTransaction extends Transaction {
return this.getCreator();
}
public Account getNewOwner() {
return new Account(this.repository, this.updateNameTransactionData.getNewOwner());
private synchronized String getReducedNewName() {
if (this.updateNameTransactionData.getReducedNewName() == null) {
String reducedNewName = Name.reduceName(this.updateNameTransactionData.getNewName());
this.updateNameTransactionData.setReducedNewName(reducedNewName);
}
return this.updateNameTransactionData.getReducedNewName();
}
// Processing
@ -51,22 +56,13 @@ public class UpdateNameTransaction extends Transaction {
public ValidationResult isValid() throws DataException {
String name = this.updateNameTransactionData.getName();
// Check new owner address is valid
if (!Crypto.isValidAddress(this.updateNameTransactionData.getNewOwner()))
return ValidationResult.INVALID_ADDRESS;
// Check name size bounds
int nameLength = Utf8.encodedLength(name);
if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE)
if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE)
return ValidationResult.INVALID_NAME_LENGTH;
// Check new data size bounds
int newDataLength = Utf8.encodedLength(this.updateNameTransactionData.getNewData());
if (newDataLength < 1 || newDataLength > Name.MAX_DATA_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
// Check name is lowercase
if (!name.equals(name.toLowerCase()))
// Check name is in normalized form (no leading/trailing whitespace, etc.)
if (!name.equals(Unicode.normalize(name)))
return ValidationResult.NAME_NOT_LOWER_CASE;
NameData nameData = this.repository.getNameRepository().fromName(name);
@ -79,12 +75,33 @@ public class UpdateNameTransaction extends Transaction {
if (nameData.getCreationGroupId() != this.updateNameTransactionData.getTxGroupId())
return ValidationResult.TX_GROUP_ID_MISMATCH;
// Check new name (0 length means don't update name)
String newName = this.updateNameTransactionData.getNewName();
int newNameLength = Utf8.encodedLength(newName);
if (newNameLength != 0) {
// Check new name size bounds
if (newNameLength < Name.MIN_NAME_SIZE || newNameLength > Name.MAX_NAME_SIZE)
return ValidationResult.INVALID_NAME_LENGTH;
// Check new name is in normalized form (no leading/trailing whitespace, etc.)
if (!newName.equals(Unicode.normalize(newName)))
return ValidationResult.NAME_NOT_LOWER_CASE;
}
// Check new data size bounds (0 length means don't update data)
int newDataLength = Utf8.encodedLength(this.updateNameTransactionData.getNewData());
if (newDataLength > Name.MAX_DATA_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
Account owner = getOwner();
// Check owner has enough funds
if (owner.getConfirmedBalance(Asset.QORT) < this.updateNameTransactionData.getFee())
return ValidationResult.NO_BALANCE;
// Fill in missing reduced new name. Caller is likely to save this as next step.
getReducedNewName();
return ValidationResult.OK;
}
@ -92,8 +109,12 @@ public class UpdateNameTransaction extends Transaction {
public ValidationResult isProcessable() throws DataException {
NameData nameData = this.repository.getNameRepository().fromName(this.updateNameTransactionData.getName());
// Check name still exists
if (nameData == null)
return ValidationResult.NAME_DOES_NOT_EXIST;
// Check name isn't currently for sale
if (nameData.getIsForSale())
if (nameData.isForSale())
return ValidationResult.NAME_ALREADY_FOR_SALE;
Account owner = getOwner();
@ -102,6 +123,11 @@ public class UpdateNameTransaction extends Transaction {
if (!owner.getAddress().equals(nameData.getOwner()))
return ValidationResult.INVALID_NAME_OWNER;
// Check new name isn't already taken, unless it is the same name (this allows for case-adjusting renames)
NameData newNameData = this.repository.getNameRepository().fromReducedName(getReducedNewName());
if (newNameData != null && !newNameData.getName().equals(nameData.getName()))
return ValidationResult.NAME_ALREADY_REGISTERED;
return ValidationResult.OK;
}
@ -111,17 +137,22 @@ public class UpdateNameTransaction extends Transaction {
Name name = new Name(this.repository, this.updateNameTransactionData.getName());
name.update(this.updateNameTransactionData);
// Save this transaction, now with updated "name reference" to previous transaction that updated name
// Save this transaction, now with updated "name reference" to previous transaction that changed name
this.repository.getTransactionRepository().save(this.updateNameTransactionData);
}
@Override
public void orphan() throws DataException {
// Revert name
Name name = new Name(this.repository, this.updateNameTransactionData.getName());
// Revert update
String nameToRevert = this.updateNameTransactionData.getNewName();
if (nameToRevert.isEmpty())
nameToRevert = this.updateNameTransactionData.getName();
Name name = new Name(this.repository, nameToRevert);
name.revert(this.updateNameTransactionData);
// Save this transaction, now with removed "name reference"
// Save this transaction, with previous "name reference"
this.repository.getTransactionRepository().save(this.updateNameTransactionData);
}

View File

@ -33,7 +33,6 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer {
layout.add("transaction's groupID", TransformationType.INT);
layout.add("reference", TransformationType.SIGNATURE);
layout.add("name registrant's public key", TransformationType.PUBLIC_KEY);
layout.add("name owner", TransformationType.ADDRESS);
layout.add("name length", TransformationType.INT);
layout.add("name", TransformationType.STRING);
layout.add("data length", TransformationType.INT);
@ -52,8 +51,6 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer {
byte[] registrantPublicKey = Serialization.deserializePublicKey(byteBuffer);
String owner = Serialization.deserializeAddress(byteBuffer);
String name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE);
String data = Serialization.deserializeSizedString(byteBuffer, Name.MAX_DATA_SIZE);
@ -65,7 +62,7 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer {
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, registrantPublicKey, fee, signature);
return new RegisterNameTransactionData(baseTransactionData, owner, name, data);
return new RegisterNameTransactionData(baseTransactionData, name, data);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
@ -83,8 +80,6 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer {
transformCommonBytes(transactionData, bytes);
Serialization.serializeAddress(bytes, registerNameTransactionData.getOwner());
Serialization.serializeSizedString(bytes, registerNameTransactionData.getName());
Serialization.serializeSizedString(bytes, registerNameTransactionData.getData());

View File

@ -18,11 +18,11 @@ import com.google.common.primitives.Longs;
public class UpdateNameTransactionTransformer extends TransactionTransformer {
// Property lengths
private static final int OWNER_LENGTH = ADDRESS_LENGTH;
private static final int NAME_SIZE_LENGTH = INT_LENGTH;
private static final int DATA_SIZE_LENGTH = INT_LENGTH;
private static final int NEW_NAME_SIZE_LENGTH = INT_LENGTH;
private static final int NEW_DATA_SIZE_LENGTH = INT_LENGTH;
private static final int EXTRAS_LENGTH = OWNER_LENGTH + NAME_SIZE_LENGTH + DATA_SIZE_LENGTH;
private static final int EXTRAS_LENGTH = NAME_SIZE_LENGTH + NEW_NAME_SIZE_LENGTH + NEW_DATA_SIZE_LENGTH;
protected static final TransactionLayout layout;
@ -33,10 +33,11 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer {
layout.add("transaction's groupID", TransformationType.INT);
layout.add("reference", TransformationType.SIGNATURE);
layout.add("name owner's public key", TransformationType.PUBLIC_KEY);
layout.add("name's new owner", TransformationType.ADDRESS);
layout.add("name length", TransformationType.INT);
layout.add("name", TransformationType.STRING);
layout.add("new data length", TransformationType.INT);
layout.add("new name's length (0 for no change)", TransformationType.INT);
layout.add("new name", TransformationType.STRING);
layout.add("new data length (0 for no change)", TransformationType.INT);
layout.add("new data", TransformationType.STRING);
layout.add("fee", TransformationType.AMOUNT);
layout.add("signature", TransformationType.SIGNATURE);
@ -52,10 +53,10 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer {
byte[] ownerPublicKey = Serialization.deserializePublicKey(byteBuffer);
String newOwner = Serialization.deserializeAddress(byteBuffer);
String name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE);
String newName = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE);
String newData = Serialization.deserializeSizedString(byteBuffer, Name.MAX_DATA_SIZE);
long fee = byteBuffer.getLong();
@ -65,13 +66,14 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer {
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, ownerPublicKey, fee, signature);
return new UpdateNameTransactionData(baseTransactionData, newOwner, name, newData);
return new UpdateNameTransactionData(baseTransactionData, name, newName, newData);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
return getBaseLength(transactionData) + EXTRAS_LENGTH + Utf8.encodedLength(updateNameTransactionData.getName())
+ Utf8.encodedLength(updateNameTransactionData.getNewName())
+ Utf8.encodedLength(updateNameTransactionData.getNewData());
}
@ -83,10 +85,10 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer {
transformCommonBytes(transactionData, bytes);
Serialization.serializeAddress(bytes, updateNameTransactionData.getNewOwner());
Serialization.serializeSizedString(bytes, updateNameTransactionData.getName());
Serialization.serializeSizedString(bytes, updateNameTransactionData.getNewName());
Serialization.serializeSizedString(bytes, updateNameTransactionData.getNewData());
bytes.write(Longs.toByteArray(updateNameTransactionData.getFee()));

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

@ -46,7 +46,7 @@ public class NamesApiTests extends ApiCommon {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String name = "test-name";
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}");
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
TransactionUtils.signAndMint(repository, transactionData, alice);
assertNotNull(this.namesResource.getNamesByAddress(alice.getAddress(), null, null, null));
@ -61,7 +61,7 @@ public class NamesApiTests extends ApiCommon {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String name = "test-name";
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}");
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
TransactionUtils.signAndMint(repository, transactionData, alice);
assertNotNull(this.namesResource.getName(name));
@ -76,7 +76,7 @@ public class NamesApiTests extends ApiCommon {
String name = "test-name";
long price = 1_23456789L;
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}");
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
TransactionUtils.signAndMint(repository, transactionData, alice);
// Sell-name

View File

@ -9,14 +9,13 @@ import org.qortal.repository.Repository;
public class RegisterNameTestTransaction extends TestTransaction {
public static TransactionData randomTransaction(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException {
String owner = account.getAddress();
String name = "test name";
if (!wantValid)
name += " " + random.nextInt(1_000_000);
String data = "{ \"key\": \"value\" }";
return new RegisterNameTransactionData(generateBase(account), owner, name, data);
return new RegisterNameTransactionData(generateBase(account), name, data);
}
}

View File

@ -61,7 +61,7 @@ public class BuySellTests extends Common {
@Test
public void testRegisterName() throws DataException {
// Register-name
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}");
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
TransactionUtils.signAndMint(repository, transactionData, alice);
String name = transactionData.getName();
@ -95,7 +95,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
assertTrue(nameData.getIsForSale());
assertTrue(nameData.isForSale());
assertEquals("price incorrect", price, nameData.getSalePrice());
// Orphan sell-name
@ -103,7 +103,7 @@ public class BuySellTests extends Common {
// Check name no longer for sale
nameData = repository.getNameRepository().fromName(name);
assertFalse(nameData.getIsForSale());
assertFalse(nameData.isForSale());
// Not concerned about price
// Re-process sell-name
@ -111,7 +111,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
assertTrue(nameData.getIsForSale());
assertTrue(nameData.isForSale());
assertEquals("price incorrect", price, nameData.getSalePrice());
// Orphan sell-name and register-name
@ -133,7 +133,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
assertTrue(nameData.getIsForSale());
assertTrue(nameData.isForSale());
assertEquals("price incorrect", price, nameData.getSalePrice());
}
@ -150,7 +150,7 @@ public class BuySellTests extends Common {
// Check name is no longer for sale
nameData = repository.getNameRepository().fromName(name);
assertFalse(nameData.getIsForSale());
assertFalse(nameData.isForSale());
// Not concerned about price
// Orphan cancel sell-name
@ -158,7 +158,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
assertTrue(nameData.getIsForSale());
assertTrue(nameData.isForSale());
assertEquals("price incorrect", price, nameData.getSalePrice());
}
@ -177,7 +177,7 @@ public class BuySellTests extends Common {
// Check name is sold
nameData = repository.getNameRepository().fromName(name);
assertFalse(nameData.getIsForSale());
assertFalse(nameData.isForSale());
// Not concerned about price
// Orphan buy-name
@ -185,7 +185,7 @@ public class BuySellTests extends Common {
// Check name is for sale (not sold)
nameData = repository.getNameRepository().fromName(name);
assertTrue(nameData.getIsForSale());
assertTrue(nameData.isForSale());
assertEquals("price incorrect", price, nameData.getSalePrice());
// Re-process buy-name
@ -193,7 +193,7 @@ public class BuySellTests extends Common {
// Check name is sold
nameData = repository.getNameRepository().fromName(name);
assertFalse(nameData.getIsForSale());
assertFalse(nameData.isForSale());
// Not concerned about price
assertEquals(bob.getAddress(), nameData.getOwner());
@ -202,7 +202,7 @@ public class BuySellTests extends Common {
// Check name no longer for sale
nameData = repository.getNameRepository().fromName(name);
assertFalse(nameData.getIsForSale());
assertFalse(nameData.isForSale());
// Not concerned about price
assertEquals(alice.getAddress(), nameData.getOwner());
@ -214,7 +214,7 @@ public class BuySellTests extends Common {
// Check name is sold
nameData = repository.getNameRepository().fromName(name);
assertFalse(nameData.getIsForSale());
assertFalse(nameData.isForSale());
// Not concerned about price
assertEquals(bob.getAddress(), nameData.getOwner());
}
@ -233,7 +233,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
assertTrue(nameData.getIsForSale());
assertTrue(nameData.isForSale());
assertEquals("price incorrect", newPrice, nameData.getSalePrice());
// Orphan sell-name
@ -241,7 +241,7 @@ public class BuySellTests extends Common {
// Check name no longer for sale
nameData = repository.getNameRepository().fromName(name);
assertFalse(nameData.getIsForSale());
assertFalse(nameData.isForSale());
// Not concerned about price
// Re-process sell-name
@ -249,7 +249,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
assertTrue(nameData.getIsForSale());
assertTrue(nameData.isForSale());
assertEquals("price incorrect", newPrice, nameData.getSalePrice());
// Orphan sell-name and buy-name
@ -257,7 +257,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
assertTrue(nameData.getIsForSale());
assertTrue(nameData.isForSale());
// Note: original sale price
assertEquals("price incorrect", price, nameData.getSalePrice());
assertEquals(alice.getAddress(), nameData.getOwner());
@ -273,7 +273,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
assertTrue(nameData.getIsForSale());
assertTrue(nameData.isForSale());
assertEquals("price incorrect", newPrice, nameData.getSalePrice());
assertEquals(bob.getAddress(), nameData.getOwner());
}

View File

@ -8,12 +8,16 @@ import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.data.transaction.RegisterNameTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.UpdateNameTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ValidationResult;
public class MiscTests extends Common {
@ -27,9 +31,10 @@ public class MiscTests extends Common {
try (final Repository repository = RepositoryManager.getRepository()) {
// Register-name
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String name = "test-name";
String name = "initial-name";
String data = "initial-data";
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}");
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
TransactionUtils.signAndMint(repository, transactionData, alice);
List<String> recentNames = repository.getNameRepository().getRecentNames(0L);
@ -39,4 +44,56 @@ public class MiscTests extends Common {
}
}
// test trying to register same name twice
@Test
public void testDuplicateRegisterName() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
// Register-name
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String name = "test-name";
String data = "{}";
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
TransactionUtils.signAndMint(repository, transactionData, alice);
// duplicate
String duplicateName = "TEST-nÁme";
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), duplicateName, data);
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(alice);
ValidationResult result = transaction.importAsUnconfirmed();
assertTrue("Transaction should be invalid", ValidationResult.OK != result);
}
}
// test register then trying to update another name to existing name
@Test
public void testUpdateToExistingName() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
// Register-name
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String name = "test-name";
String data = "{}";
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
TransactionUtils.signAndMint(repository, transactionData, alice);
// Register another name that we will later attempt to rename to first name (above)
String otherName = "new-name";
String otherData = "";
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), otherName, otherData);
TransactionUtils.signAndMint(repository, transactionData, alice);
// we shouldn't be able to update name to existing name
String duplicateName = "TEST-nÁme";
transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), otherName, duplicateName, otherData);
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(alice);
ValidationResult result = transaction.importAsUnconfirmed();
assertTrue("Transaction should be invalid", ValidationResult.OK != result);
}
}
}

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