getRecipientAddresses() throws DataException {
- return Collections.singletonList(this.updateNameTransactionData.getNewOwner());
+ return Collections.emptyList();
}
// Navigation
@@ -41,8 +41,13 @@ public class UpdateNameTransaction extends Transaction {
return this.getCreator();
}
- public Account getNewOwner() {
- return new Account(this.repository, this.updateNameTransactionData.getNewOwner());
+ private synchronized String getReducedNewName() {
+ if (this.updateNameTransactionData.getReducedNewName() == null) {
+ String reducedNewName = Name.reduceName(this.updateNameTransactionData.getNewName());
+ this.updateNameTransactionData.setReducedNewName(reducedNewName);
+ }
+
+ return this.updateNameTransactionData.getReducedNewName();
}
// Processing
@@ -51,22 +56,13 @@ public class UpdateNameTransaction extends Transaction {
public ValidationResult isValid() throws DataException {
String name = this.updateNameTransactionData.getName();
- // Check new owner address is valid
- if (!Crypto.isValidAddress(this.updateNameTransactionData.getNewOwner()))
- return ValidationResult.INVALID_ADDRESS;
-
// Check name size bounds
int nameLength = Utf8.encodedLength(name);
- if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE)
+ if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE)
return ValidationResult.INVALID_NAME_LENGTH;
- // Check new data size bounds
- int newDataLength = Utf8.encodedLength(this.updateNameTransactionData.getNewData());
- if (newDataLength < 1 || newDataLength > Name.MAX_DATA_SIZE)
- return ValidationResult.INVALID_DATA_LENGTH;
-
- // Check name is lowercase
- if (!name.equals(name.toLowerCase()))
+ // Check name is in normalized form (no leading/trailing whitespace, etc.)
+ if (!name.equals(Unicode.normalize(name)))
return ValidationResult.NAME_NOT_LOWER_CASE;
NameData nameData = this.repository.getNameRepository().fromName(name);
@@ -79,12 +75,33 @@ public class UpdateNameTransaction extends Transaction {
if (nameData.getCreationGroupId() != this.updateNameTransactionData.getTxGroupId())
return ValidationResult.TX_GROUP_ID_MISMATCH;
+ // Check new name (0 length means don't update name)
+ String newName = this.updateNameTransactionData.getNewName();
+ int newNameLength = Utf8.encodedLength(newName);
+ if (newNameLength != 0) {
+ // Check new name size bounds
+ if (newNameLength < Name.MIN_NAME_SIZE || newNameLength > Name.MAX_NAME_SIZE)
+ return ValidationResult.INVALID_NAME_LENGTH;
+
+ // Check new name is in normalized form (no leading/trailing whitespace, etc.)
+ if (!newName.equals(Unicode.normalize(newName)))
+ return ValidationResult.NAME_NOT_LOWER_CASE;
+ }
+
+ // Check new data size bounds (0 length means don't update data)
+ int newDataLength = Utf8.encodedLength(this.updateNameTransactionData.getNewData());
+ if (newDataLength > Name.MAX_DATA_SIZE)
+ return ValidationResult.INVALID_DATA_LENGTH;
+
Account owner = getOwner();
// Check owner has enough funds
if (owner.getConfirmedBalance(Asset.QORT) < this.updateNameTransactionData.getFee())
return ValidationResult.NO_BALANCE;
+ // Fill in missing reduced new name. Caller is likely to save this as next step.
+ getReducedNewName();
+
return ValidationResult.OK;
}
@@ -92,8 +109,12 @@ public class UpdateNameTransaction extends Transaction {
public ValidationResult isProcessable() throws DataException {
NameData nameData = this.repository.getNameRepository().fromName(this.updateNameTransactionData.getName());
+ // Check name still exists
+ if (nameData == null)
+ return ValidationResult.NAME_DOES_NOT_EXIST;
+
// Check name isn't currently for sale
- if (nameData.getIsForSale())
+ if (nameData.isForSale())
return ValidationResult.NAME_ALREADY_FOR_SALE;
Account owner = getOwner();
@@ -102,6 +123,11 @@ public class UpdateNameTransaction extends Transaction {
if (!owner.getAddress().equals(nameData.getOwner()))
return ValidationResult.INVALID_NAME_OWNER;
+ // Check new name isn't already taken, unless it is the same name (this allows for case-adjusting renames)
+ NameData newNameData = this.repository.getNameRepository().fromReducedName(getReducedNewName());
+ if (newNameData != null && !newNameData.getName().equals(nameData.getName()))
+ return ValidationResult.NAME_ALREADY_REGISTERED;
+
return ValidationResult.OK;
}
@@ -111,17 +137,22 @@ public class UpdateNameTransaction extends Transaction {
Name name = new Name(this.repository, this.updateNameTransactionData.getName());
name.update(this.updateNameTransactionData);
- // Save this transaction, now with updated "name reference" to previous transaction that updated name
+ // Save this transaction, now with updated "name reference" to previous transaction that changed name
this.repository.getTransactionRepository().save(this.updateNameTransactionData);
}
@Override
public void orphan() throws DataException {
- // Revert name
- Name name = new Name(this.repository, this.updateNameTransactionData.getName());
+ // Revert update
+
+ String nameToRevert = this.updateNameTransactionData.getNewName();
+ if (nameToRevert.isEmpty())
+ nameToRevert = this.updateNameTransactionData.getName();
+
+ Name name = new Name(this.repository, nameToRevert);
name.revert(this.updateNameTransactionData);
- // Save this transaction, now with removed "name reference"
+ // Save this transaction, with previous "name reference"
this.repository.getTransactionRepository().save(this.updateNameTransactionData);
}
diff --git a/src/main/java/org/qortal/transform/transaction/RegisterNameTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/RegisterNameTransactionTransformer.java
index 1bf4057e..6722c601 100644
--- a/src/main/java/org/qortal/transform/transaction/RegisterNameTransactionTransformer.java
+++ b/src/main/java/org/qortal/transform/transaction/RegisterNameTransactionTransformer.java
@@ -33,7 +33,6 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer {
layout.add("transaction's groupID", TransformationType.INT);
layout.add("reference", TransformationType.SIGNATURE);
layout.add("name registrant's public key", TransformationType.PUBLIC_KEY);
- layout.add("name owner", TransformationType.ADDRESS);
layout.add("name length", TransformationType.INT);
layout.add("name", TransformationType.STRING);
layout.add("data length", TransformationType.INT);
@@ -52,8 +51,6 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer {
byte[] registrantPublicKey = Serialization.deserializePublicKey(byteBuffer);
- String owner = Serialization.deserializeAddress(byteBuffer);
-
String name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE);
String data = Serialization.deserializeSizedString(byteBuffer, Name.MAX_DATA_SIZE);
@@ -65,7 +62,7 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer {
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, registrantPublicKey, fee, signature);
- return new RegisterNameTransactionData(baseTransactionData, owner, name, data);
+ return new RegisterNameTransactionData(baseTransactionData, name, data);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
@@ -83,8 +80,6 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer {
transformCommonBytes(transactionData, bytes);
- Serialization.serializeAddress(bytes, registerNameTransactionData.getOwner());
-
Serialization.serializeSizedString(bytes, registerNameTransactionData.getName());
Serialization.serializeSizedString(bytes, registerNameTransactionData.getData());
diff --git a/src/main/java/org/qortal/transform/transaction/UpdateNameTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/UpdateNameTransactionTransformer.java
index 56a4f988..6e3aa06b 100644
--- a/src/main/java/org/qortal/transform/transaction/UpdateNameTransactionTransformer.java
+++ b/src/main/java/org/qortal/transform/transaction/UpdateNameTransactionTransformer.java
@@ -18,11 +18,11 @@ import com.google.common.primitives.Longs;
public class UpdateNameTransactionTransformer extends TransactionTransformer {
// Property lengths
- private static final int OWNER_LENGTH = ADDRESS_LENGTH;
private static final int NAME_SIZE_LENGTH = INT_LENGTH;
- private static final int DATA_SIZE_LENGTH = INT_LENGTH;
+ private static final int NEW_NAME_SIZE_LENGTH = INT_LENGTH;
+ private static final int NEW_DATA_SIZE_LENGTH = INT_LENGTH;
- private static final int EXTRAS_LENGTH = OWNER_LENGTH + NAME_SIZE_LENGTH + DATA_SIZE_LENGTH;
+ private static final int EXTRAS_LENGTH = NAME_SIZE_LENGTH + NEW_NAME_SIZE_LENGTH + NEW_DATA_SIZE_LENGTH;
protected static final TransactionLayout layout;
@@ -33,10 +33,11 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer {
layout.add("transaction's groupID", TransformationType.INT);
layout.add("reference", TransformationType.SIGNATURE);
layout.add("name owner's public key", TransformationType.PUBLIC_KEY);
- layout.add("name's new owner", TransformationType.ADDRESS);
layout.add("name length", TransformationType.INT);
layout.add("name", TransformationType.STRING);
- layout.add("new data length", TransformationType.INT);
+ layout.add("new name's length (0 for no change)", TransformationType.INT);
+ layout.add("new name", TransformationType.STRING);
+ layout.add("new data length (0 for no change)", TransformationType.INT);
layout.add("new data", TransformationType.STRING);
layout.add("fee", TransformationType.AMOUNT);
layout.add("signature", TransformationType.SIGNATURE);
@@ -52,10 +53,10 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer {
byte[] ownerPublicKey = Serialization.deserializePublicKey(byteBuffer);
- String newOwner = Serialization.deserializeAddress(byteBuffer);
-
String name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE);
+ String newName = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE);
+
String newData = Serialization.deserializeSizedString(byteBuffer, Name.MAX_DATA_SIZE);
long fee = byteBuffer.getLong();
@@ -65,13 +66,14 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer {
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, ownerPublicKey, fee, signature);
- return new UpdateNameTransactionData(baseTransactionData, newOwner, name, newData);
+ return new UpdateNameTransactionData(baseTransactionData, name, newName, newData);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
return getBaseLength(transactionData) + EXTRAS_LENGTH + Utf8.encodedLength(updateNameTransactionData.getName())
+ + Utf8.encodedLength(updateNameTransactionData.getNewName())
+ Utf8.encodedLength(updateNameTransactionData.getNewData());
}
@@ -83,10 +85,10 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer {
transformCommonBytes(transactionData, bytes);
- Serialization.serializeAddress(bytes, updateNameTransactionData.getNewOwner());
-
Serialization.serializeSizedString(bytes, updateNameTransactionData.getName());
+ Serialization.serializeSizedString(bytes, updateNameTransactionData.getNewName());
+
Serialization.serializeSizedString(bytes, updateNameTransactionData.getNewData());
bytes.write(Longs.toByteArray(updateNameTransactionData.getFee()));
diff --git a/src/main/java/org/qortal/utils/Unicode.java b/src/main/java/org/qortal/utils/Unicode.java
new file mode 100644
index 00000000..8a9092ea
--- /dev/null
+++ b/src/main/java/org/qortal/utils/Unicode.java
@@ -0,0 +1,220 @@
+package org.qortal.utils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UncheckedIOException;
+import java.text.Normalizer;
+import java.text.Normalizer.Form;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.MissingResourceException;
+import java.util.TreeMap;
+
+import com.google.common.base.CharMatcher;
+
+import net.codebox.homoglyph.HomoglyphBuilder;
+
+public abstract class Unicode {
+
+ public static final String NO_BREAK_SPACE = "\u00a0";
+
+ public static final String ZERO_WIDTH_SPACE = "\u200b";
+ public static final String ZERO_WIDTH_NON_JOINER = "\u200c";
+ public static final String ZERO_WIDTH_JOINER = "\u200d";
+ public static final String WORD_JOINER = "\u2060";
+ public static final String ZERO_WIDTH_NO_BREAK_SPACE = "\ufeff";
+
+ public static final CharMatcher ZERO_WIDTH_CHAR_MATCHER = CharMatcher.anyOf(ZERO_WIDTH_SPACE + ZERO_WIDTH_NON_JOINER + ZERO_WIDTH_JOINER + WORD_JOINER + ZERO_WIDTH_NO_BREAK_SPACE);
+
+ private static int[] homoglyphCodePoints;
+ private static int[] reducedCodePoints;
+
+ private static final String CHAR_CODES_FILE = "/char_codes.txt";
+
+ static {
+ buildHomoglyphCodePointArrays();
+ }
+
+ /** Returns string in Unicode canonical normalized form (NFC),
+ * with zero-width spaces/joiners removed,
+ * leading/trailing whitespace trimmed
+ * and all other whitespace blocks collapsed into a single space character.
+ *
+ * Example: [ZWS] means zero-width space
+ *
+ * - " powdered [TAB] to[ZWS]ast " becomes "powdered toast"
+ *
+ *
+ * @see Form#NFKC
+ * @see Unicode#removeZeroWidth(String)
+ * @see CharMatcher#whitespace()
+ * @see CharMatcher#trimAndCollapseFrom(CharSequence, char)
+ */
+ public static String normalize(String input) {
+ String output;
+
+ // Normalize
+ output = Normalizer.normalize(input, Form.NFKC);
+
+ // Remove zero-width code-points, used for rendering
+ output = removeZeroWidth(output);
+
+ // Normalize whitespace
+ output = CharMatcher.whitespace().trimAndCollapseFrom(output, ' ');
+
+ return output;
+ }
+
+ /** Returns string after normalization,
+ * conversion to lowercase (locale insensitive)
+ * and homoglyphs replaced with simpler, reduced codepoints.
+ *
+ * Example:
+ *
+ * - " TΟÁST " becomes "toast"
+ *
+ *
+ * @see Form#NFKC
+ * @see Unicode#removeZeroWidth(String)
+ * @see CharMatcher#whitespace()
+ * @see CharMatcher#trimAndCollapseFrom(CharSequence, char)
+ * @see String#toLowerCase(Locale)
+ * @see Locale#ROOT
+ * @see Unicode#reduceHomoglyphs(String)
+ */
+ public static String sanitize(String input) {
+ String output;
+
+ // Normalize
+ output = Normalizer.normalize(input, Form.NFKD);
+
+ // Remove zero-width code-points, used for rendering
+ output = removeZeroWidth(output);
+
+ // Normalize whitespace
+ output = CharMatcher.whitespace().trimAndCollapseFrom(output, ' ');
+
+ // Remove accents, combining marks
+ output = output.replaceAll("[\\p{M}\\p{C}]", "");
+
+ // Convert to lowercase
+ output = output.toLowerCase(Locale.ROOT);
+
+ // Reduce homoglyphs
+ output = reduceHomoglyphs(output);
+
+ return output;
+ }
+
+ public static String removeZeroWidth(String input) {
+ return ZERO_WIDTH_CHAR_MATCHER.removeFrom(input);
+ }
+
+ public static String reduceHomoglyphs(String input) {
+ CodePoints codePoints = new CodePoints(input);
+ final int length = codePoints.getLength();
+
+ for (int i = 0; i < length; ++i) {
+ int inputCodePoint = codePoints.getValue(i);
+
+ int index = Arrays.binarySearch(homoglyphCodePoints, inputCodePoint);
+ if (index >= 0)
+ codePoints.setValue(i, reducedCodePoints[index]);
+ }
+
+ return codePoints.toString();
+ }
+
+ private static void buildHomoglyphCodePointArrays() {
+ final InputStream is = HomoglyphBuilder.class.getResourceAsStream(CHAR_CODES_FILE);
+
+ if (is == null)
+ throw new MissingResourceException("Unable to read " + CHAR_CODES_FILE, HomoglyphBuilder.class.getName(),
+ CHAR_CODES_FILE);
+
+ final Reader reader = new InputStreamReader(is);
+
+ Map homoglyphReductions = new TreeMap<>();
+
+ try (final BufferedReader bufferedReader = new BufferedReader(reader)) {
+ String line;
+
+ while ((line = bufferedReader.readLine()) != null) {
+ line = line.trim();
+
+ if (line.startsWith("#") || line.length() == 0)
+ continue;
+
+ String[] charCodes = line.split(",");
+
+ // We consider the first charCode to be the 'reduced' form
+ int reducedCodepoint;
+ try {
+ reducedCodepoint = Integer.parseInt(charCodes[0], 16);
+ } catch (NumberFormatException ex) {
+ // ignore badly formatted lines
+ continue;
+ }
+
+ // Map remaining charCodes
+ for (int i = 1; i < charCodes.length; ++i)
+ try {
+ int homoglyphCodepoint = Integer.parseInt(charCodes[i], 16);
+
+ homoglyphReductions.put(homoglyphCodepoint, reducedCodepoint);
+ } catch (NumberFormatException ex) {
+ // ignore
+ }
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+
+ homoglyphCodePoints = homoglyphReductions.keySet().stream().mapToInt(i -> i).toArray();
+ reducedCodePoints = homoglyphReductions.values().stream().mapToInt(i -> i).toArray();
+ }
+
+ private static class CodePoints {
+ private final int[] codepointArray;
+
+ public CodePoints(String text) {
+ final List codepointList = new ArrayList<>();
+
+ int codepoint;
+ for (int offset = 0; offset < text.length(); offset += Character.charCount(codepoint)) {
+ codepoint = text.codePointAt(offset);
+ codepointList.add(codepoint);
+ }
+
+ this.codepointArray = codepointList.stream().mapToInt(i -> i).toArray();
+ }
+
+ public int getValue(int i) {
+ return codepointArray[i];
+ }
+
+ public void setValue(int i, int codepoint) {
+ codepointArray[i] = codepoint;
+ }
+
+ public int getLength() {
+ return codepointArray.length;
+ }
+
+ public String toString() {
+ final StringBuilder sb = new StringBuilder(this.codepointArray.length);
+
+ for (int i = 0; i < this.codepointArray.length; i++)
+ sb.appendCodePoint(this.codepointArray[i]);
+
+ return sb.toString();
+ }
+ }
+
+}
diff --git a/src/test/java/org/qortal/test/UnicodeTests.java b/src/test/java/org/qortal/test/UnicodeTests.java
new file mode 100644
index 00000000..2e0f7968
--- /dev/null
+++ b/src/test/java/org/qortal/test/UnicodeTests.java
@@ -0,0 +1,38 @@
+package org.qortal.test;
+
+import static org.junit.Assert.*;
+import static org.qortal.utils.Unicode.*;
+
+import org.junit.Test;
+import org.qortal.utils.Unicode;
+
+public class UnicodeTests {
+
+ @Test
+ public void testWhitespace() {
+ String input = " " + NO_BREAK_SPACE + "test ";
+
+ String output = Unicode.normalize(input);
+
+ assertEquals("trim & collapse failed", "test", output);
+ }
+
+ @Test
+ public void testCaseComparison() {
+ String input1 = " " + NO_BREAK_SPACE + "test ";
+ String input2 = " " + NO_BREAK_SPACE + "TEST " + ZERO_WIDTH_SPACE;
+
+ assertEquals("strings should match", Unicode.sanitize(input1), Unicode.sanitize(input2));
+ }
+
+ @Test
+ public void testHomoglyph() {
+ String omicron = "\u03bf";
+
+ String input1 = " " + NO_BREAK_SPACE + "toÁst ";
+ String input2 = " " + NO_BREAK_SPACE + "t" + omicron + "ast " + ZERO_WIDTH_SPACE;
+
+ assertEquals("strings should match", Unicode.sanitize(input1), Unicode.sanitize(input2));
+ }
+
+}
diff --git a/src/test/java/org/qortal/test/api/NamesApiTests.java b/src/test/java/org/qortal/test/api/NamesApiTests.java
index ae7248b4..2d31c8c2 100644
--- a/src/test/java/org/qortal/test/api/NamesApiTests.java
+++ b/src/test/java/org/qortal/test/api/NamesApiTests.java
@@ -46,7 +46,7 @@ public class NamesApiTests extends ApiCommon {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String name = "test-name";
- RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}");
+ RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
TransactionUtils.signAndMint(repository, transactionData, alice);
assertNotNull(this.namesResource.getNamesByAddress(alice.getAddress(), null, null, null));
@@ -61,7 +61,7 @@ public class NamesApiTests extends ApiCommon {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String name = "test-name";
- RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}");
+ RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
TransactionUtils.signAndMint(repository, transactionData, alice);
assertNotNull(this.namesResource.getName(name));
@@ -76,7 +76,7 @@ public class NamesApiTests extends ApiCommon {
String name = "test-name";
long price = 1_23456789L;
- TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}");
+ TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
TransactionUtils.signAndMint(repository, transactionData, alice);
// Sell-name
diff --git a/src/test/java/org/qortal/test/common/transaction/RegisterNameTestTransaction.java b/src/test/java/org/qortal/test/common/transaction/RegisterNameTestTransaction.java
index 14f93657..0450fbee 100644
--- a/src/test/java/org/qortal/test/common/transaction/RegisterNameTestTransaction.java
+++ b/src/test/java/org/qortal/test/common/transaction/RegisterNameTestTransaction.java
@@ -9,14 +9,13 @@ import org.qortal.repository.Repository;
public class RegisterNameTestTransaction extends TestTransaction {
public static TransactionData randomTransaction(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException {
- String owner = account.getAddress();
String name = "test name";
if (!wantValid)
name += " " + random.nextInt(1_000_000);
String data = "{ \"key\": \"value\" }";
- return new RegisterNameTransactionData(generateBase(account), owner, name, data);
+ return new RegisterNameTransactionData(generateBase(account), name, data);
}
}
diff --git a/src/test/java/org/qortal/test/naming/BuySellTests.java b/src/test/java/org/qortal/test/naming/BuySellTests.java
index 0d323c5b..f0320da5 100644
--- a/src/test/java/org/qortal/test/naming/BuySellTests.java
+++ b/src/test/java/org/qortal/test/naming/BuySellTests.java
@@ -61,7 +61,7 @@ public class BuySellTests extends Common {
@Test
public void testRegisterName() throws DataException {
// Register-name
- RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}");
+ RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
TransactionUtils.signAndMint(repository, transactionData, alice);
String name = transactionData.getName();
@@ -95,7 +95,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
- assertTrue(nameData.getIsForSale());
+ assertTrue(nameData.isForSale());
assertEquals("price incorrect", price, nameData.getSalePrice());
// Orphan sell-name
@@ -103,7 +103,7 @@ public class BuySellTests extends Common {
// Check name no longer for sale
nameData = repository.getNameRepository().fromName(name);
- assertFalse(nameData.getIsForSale());
+ assertFalse(nameData.isForSale());
// Not concerned about price
// Re-process sell-name
@@ -111,7 +111,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
- assertTrue(nameData.getIsForSale());
+ assertTrue(nameData.isForSale());
assertEquals("price incorrect", price, nameData.getSalePrice());
// Orphan sell-name and register-name
@@ -133,7 +133,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
- assertTrue(nameData.getIsForSale());
+ assertTrue(nameData.isForSale());
assertEquals("price incorrect", price, nameData.getSalePrice());
}
@@ -150,7 +150,7 @@ public class BuySellTests extends Common {
// Check name is no longer for sale
nameData = repository.getNameRepository().fromName(name);
- assertFalse(nameData.getIsForSale());
+ assertFalse(nameData.isForSale());
// Not concerned about price
// Orphan cancel sell-name
@@ -158,7 +158,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
- assertTrue(nameData.getIsForSale());
+ assertTrue(nameData.isForSale());
assertEquals("price incorrect", price, nameData.getSalePrice());
}
@@ -177,7 +177,7 @@ public class BuySellTests extends Common {
// Check name is sold
nameData = repository.getNameRepository().fromName(name);
- assertFalse(nameData.getIsForSale());
+ assertFalse(nameData.isForSale());
// Not concerned about price
// Orphan buy-name
@@ -185,7 +185,7 @@ public class BuySellTests extends Common {
// Check name is for sale (not sold)
nameData = repository.getNameRepository().fromName(name);
- assertTrue(nameData.getIsForSale());
+ assertTrue(nameData.isForSale());
assertEquals("price incorrect", price, nameData.getSalePrice());
// Re-process buy-name
@@ -193,7 +193,7 @@ public class BuySellTests extends Common {
// Check name is sold
nameData = repository.getNameRepository().fromName(name);
- assertFalse(nameData.getIsForSale());
+ assertFalse(nameData.isForSale());
// Not concerned about price
assertEquals(bob.getAddress(), nameData.getOwner());
@@ -202,7 +202,7 @@ public class BuySellTests extends Common {
// Check name no longer for sale
nameData = repository.getNameRepository().fromName(name);
- assertFalse(nameData.getIsForSale());
+ assertFalse(nameData.isForSale());
// Not concerned about price
assertEquals(alice.getAddress(), nameData.getOwner());
@@ -214,7 +214,7 @@ public class BuySellTests extends Common {
// Check name is sold
nameData = repository.getNameRepository().fromName(name);
- assertFalse(nameData.getIsForSale());
+ assertFalse(nameData.isForSale());
// Not concerned about price
assertEquals(bob.getAddress(), nameData.getOwner());
}
@@ -233,7 +233,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
- assertTrue(nameData.getIsForSale());
+ assertTrue(nameData.isForSale());
assertEquals("price incorrect", newPrice, nameData.getSalePrice());
// Orphan sell-name
@@ -241,7 +241,7 @@ public class BuySellTests extends Common {
// Check name no longer for sale
nameData = repository.getNameRepository().fromName(name);
- assertFalse(nameData.getIsForSale());
+ assertFalse(nameData.isForSale());
// Not concerned about price
// Re-process sell-name
@@ -249,7 +249,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
- assertTrue(nameData.getIsForSale());
+ assertTrue(nameData.isForSale());
assertEquals("price incorrect", newPrice, nameData.getSalePrice());
// Orphan sell-name and buy-name
@@ -257,7 +257,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
- assertTrue(nameData.getIsForSale());
+ assertTrue(nameData.isForSale());
// Note: original sale price
assertEquals("price incorrect", price, nameData.getSalePrice());
assertEquals(alice.getAddress(), nameData.getOwner());
@@ -273,7 +273,7 @@ public class BuySellTests extends Common {
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
- assertTrue(nameData.getIsForSale());
+ assertTrue(nameData.isForSale());
assertEquals("price incorrect", newPrice, nameData.getSalePrice());
assertEquals(bob.getAddress(), nameData.getOwner());
}
diff --git a/src/test/java/org/qortal/test/naming/MiscTests.java b/src/test/java/org/qortal/test/naming/MiscTests.java
index d9cbf6fc..0bb750d0 100644
--- a/src/test/java/org/qortal/test/naming/MiscTests.java
+++ b/src/test/java/org/qortal/test/naming/MiscTests.java
@@ -8,12 +8,16 @@ import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.data.transaction.RegisterNameTransactionData;
+import org.qortal.data.transaction.TransactionData;
+import org.qortal.data.transaction.UpdateNameTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction;
+import org.qortal.transaction.Transaction;
+import org.qortal.transaction.Transaction.ValidationResult;
public class MiscTests extends Common {
@@ -27,9 +31,10 @@ public class MiscTests extends Common {
try (final Repository repository = RepositoryManager.getRepository()) {
// Register-name
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
- String name = "test-name";
+ String name = "initial-name";
+ String data = "initial-data";
- RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}");
+ RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
TransactionUtils.signAndMint(repository, transactionData, alice);
List recentNames = repository.getNameRepository().getRecentNames(0L);
@@ -39,4 +44,56 @@ public class MiscTests extends Common {
}
}
+ // test trying to register same name twice
+ @Test
+ public void testDuplicateRegisterName() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ // Register-name
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ String name = "test-name";
+ String data = "{}";
+
+ RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ // duplicate
+ String duplicateName = "TEST-nÁme";
+ transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), duplicateName, data);
+ Transaction transaction = Transaction.fromData(repository, transactionData);
+ transaction.sign(alice);
+
+ ValidationResult result = transaction.importAsUnconfirmed();
+ assertTrue("Transaction should be invalid", ValidationResult.OK != result);
+ }
+ }
+
+ // test register then trying to update another name to existing name
+ @Test
+ public void testUpdateToExistingName() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ // Register-name
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ String name = "test-name";
+ String data = "{}";
+
+ TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ // Register another name that we will later attempt to rename to first name (above)
+ String otherName = "new-name";
+ String otherData = "";
+ transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), otherName, otherData);
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ // we shouldn't be able to update name to existing name
+ String duplicateName = "TEST-nÁme";
+ transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), otherName, duplicateName, otherData);
+ Transaction transaction = Transaction.fromData(repository, transactionData);
+ transaction.sign(alice);
+
+ ValidationResult result = transaction.importAsUnconfirmed();
+ assertTrue("Transaction should be invalid", ValidationResult.OK != result);
+ }
+ }
+
}
diff --git a/src/test/java/org/qortal/test/naming/UpdateTests.java b/src/test/java/org/qortal/test/naming/UpdateTests.java
new file mode 100644
index 00000000..ffbf7177
--- /dev/null
+++ b/src/test/java/org/qortal/test/naming/UpdateTests.java
@@ -0,0 +1,334 @@
+package org.qortal.test.naming;
+
+import static org.junit.Assert.*;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.qortal.account.PrivateKeyAccount;
+import org.qortal.data.transaction.RegisterNameTransactionData;
+import org.qortal.data.transaction.TransactionData;
+import org.qortal.data.transaction.UpdateNameTransactionData;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
+import org.qortal.test.common.BlockUtils;
+import org.qortal.test.common.Common;
+import org.qortal.test.common.TransactionUtils;
+import org.qortal.test.common.transaction.TestTransaction;
+
+public class UpdateTests extends Common {
+
+ @Before
+ public void beforeTest() throws DataException {
+ Common.useDefaultSettings();
+ }
+
+ @Test
+ public void testUpdateName() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ // Register-name
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ String initialName = "initial-name";
+ String initialData = "initial-data";
+
+ TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
+ TransactionUtils.signAndMint(repository, initialTransactionData, alice);
+
+ String newName = "new-name";
+ String newData = "";
+ TransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newName, newData);
+ TransactionUtils.signAndMint(repository, updateTransactionData, alice);
+
+ // Check old name no longer exists
+ assertFalse(repository.getNameRepository().nameExists(initialName));
+
+ // Check new name exists
+ assertTrue(repository.getNameRepository().nameExists(newName));
+
+ // Check updated timestamp is correct
+ assertEquals((Long) updateTransactionData.getTimestamp(), repository.getNameRepository().fromName(newName).getUpdated());
+
+ // orphan and recheck
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check new name no longer exists
+ assertFalse(repository.getNameRepository().nameExists(newName));
+
+ // Check old name exists again
+ assertTrue(repository.getNameRepository().nameExists(initialName));
+
+ // Check updated timestamp is empty
+ assertNull(repository.getNameRepository().fromName(initialName).getUpdated());
+ }
+ }
+
+ @Test
+ public void testUpdateNameSameOwner() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ // Register-name
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ String initialName = "initial-name";
+ String initialData = "initial-data";
+
+ TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
+ TransactionUtils.signAndMint(repository, initialTransactionData, alice);
+
+ String newName = "Initial-Name";
+ String newData = "";
+ TransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newName, newData);
+ TransactionUtils.signAndMint(repository, updateTransactionData, alice);
+
+ // Check old name no longer exists
+ assertFalse(repository.getNameRepository().nameExists(initialName));
+
+ // Check new name exists
+ assertTrue(repository.getNameRepository().nameExists(newName));
+
+ // Check updated timestamp is correct
+ assertEquals((Long) updateTransactionData.getTimestamp(), repository.getNameRepository().fromName(newName).getUpdated());
+
+ // orphan and recheck
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check new name no longer exists
+ assertFalse(repository.getNameRepository().nameExists(newName));
+
+ // Check old name exists again
+ assertTrue(repository.getNameRepository().nameExists(initialName));
+
+ // Check updated timestamp is empty
+ assertNull(repository.getNameRepository().fromName(initialName).getUpdated());
+ }
+ }
+
+ // Test that reverting using previous UPDATE_NAME works as expected
+ @Test
+ public void testDoubleUpdateName() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ // Register-name
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ String initialName = "initial-name";
+ String initialData = "initial-data";
+
+ TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
+ TransactionUtils.signAndMint(repository, initialTransactionData, alice);
+
+ String middleName = "middle-name";
+ String middleData = "";
+ TransactionData middleTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData);
+ TransactionUtils.signAndMint(repository, middleTransactionData, alice);
+
+ // Check old name no longer exists
+ assertFalse(repository.getNameRepository().nameExists(initialName));
+
+ // Check new name exists
+ assertTrue(repository.getNameRepository().nameExists(middleName));
+
+ String newestName = "newest-name";
+ String newestData = "newest-data";
+ TransactionData newestTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), middleName, newestName, newestData);
+ TransactionUtils.signAndMint(repository, newestTransactionData, alice);
+
+ // Check previous name no longer exists
+ assertFalse(repository.getNameRepository().nameExists(middleName));
+
+ // Check newest name exists
+ assertTrue(repository.getNameRepository().nameExists(newestName));
+
+ // Check updated timestamp is correct
+ assertEquals((Long) newestTransactionData.getTimestamp(), repository.getNameRepository().fromName(newestName).getUpdated());
+
+ // orphan and recheck
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check newest name no longer exists
+ assertFalse(repository.getNameRepository().nameExists(newestName));
+
+ // Check previous name exists again
+ assertTrue(repository.getNameRepository().nameExists(middleName));
+
+ // Check updated timestamp is correct
+ assertEquals((Long) middleTransactionData.getTimestamp(), repository.getNameRepository().fromName(middleName).getUpdated());
+
+ // orphan and recheck
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check new name no longer exists
+ assertFalse(repository.getNameRepository().nameExists(middleName));
+
+ // Check original name exists again
+ assertTrue(repository.getNameRepository().nameExists(initialName));
+
+ // Check updated timestamp is empty
+ assertNull(repository.getNameRepository().fromName(initialName).getUpdated());
+ }
+ }
+
+ // Test that reverting using previous UPDATE_NAME works as expected
+ @Test
+ public void testIntermediateUpdateName() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ // Register-name
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ String initialName = "initial-name";
+ String initialData = "initial-data";
+
+ TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ // Don't update name, but update data.
+ // This tests whether reverting a future update/sale can find the correct previous name
+ String middleName = "";
+ String middleData = "middle-data";
+ transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData);
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ // Check old name still exists
+ assertTrue(repository.getNameRepository().nameExists(initialName));
+
+ String newestName = "newest-name";
+ String newestData = "newest-data";
+ transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newestName, newestData);
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ // Check previous name no longer exists
+ assertFalse(repository.getNameRepository().nameExists(initialName));
+
+ // Check newest name exists
+ assertTrue(repository.getNameRepository().nameExists(newestName));
+
+ // orphan and recheck
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check original name exists again
+ assertTrue(repository.getNameRepository().nameExists(initialName));
+
+ // orphan and recheck
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check original name still exists
+ assertTrue(repository.getNameRepository().nameExists(initialName));
+ }
+ }
+
+ @Test
+ public void testUpdateData() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ // Register-name
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ String initialName = "initial-name";
+ String initialData = "initial-data";
+
+ TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ String newName = "";
+ String newData = "new-data";
+ transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newName, newData);
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ // Check name still exists
+ assertTrue(repository.getNameRepository().nameExists(initialName));
+
+ // Check data is correct
+ assertEquals(newData, repository.getNameRepository().fromName(initialName).getData());
+
+ // orphan and recheck
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check name still exists
+ assertTrue(repository.getNameRepository().nameExists(initialName));
+
+ // Check old data restored
+ assertEquals(initialData, repository.getNameRepository().fromName(initialName).getData());
+ }
+ }
+
+ // Test that reverting using previous UPDATE_NAME works as expected
+ @Test
+ public void testDoubleUpdateData() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ // Register-name
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ String initialName = "initial-name";
+ String initialData = "initial-data";
+
+ TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ // Update data
+ String middleName = "middle-name";
+ String middleData = "middle-data";
+ transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData);
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ // Check data is correct
+ assertEquals(middleData, repository.getNameRepository().fromName(middleName).getData());
+
+ String newestName = "newest-name";
+ String newestData = "newest-data";
+ transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), middleName, newestName, newestData);
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ // Check data is correct
+ assertEquals(newestData, repository.getNameRepository().fromName(newestName).getData());
+
+ // orphan and recheck
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check data is correct
+ assertEquals(middleData, repository.getNameRepository().fromName(middleName).getData());
+
+ // orphan and recheck
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check data is correct
+ assertEquals(initialData, repository.getNameRepository().fromName(initialName).getData());
+ }
+ }
+
+ // Test that reverting using previous UPDATE_NAME works as expected
+ @Test
+ public void testIntermediateUpdateData() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ // Register-name
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ String initialName = "initial-name";
+ String initialData = "initial-data";
+
+ TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ // Don't update data, but update name.
+ // This tests whether reverting a future update/sale can find the correct previous data
+ String middleName = "middle-name";
+ String middleData = "";
+ transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData);
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ // Check data is correct
+ assertEquals(initialData, repository.getNameRepository().fromName(middleName).getData());
+
+ String newestName = "newest-name";
+ String newestData = "newest-data";
+ transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), middleName, newestName, newestData);
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ // Check data is correct
+ assertEquals(newestData, repository.getNameRepository().fromName(newestName).getData());
+
+ // orphan and recheck
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check data is correct
+ assertEquals(initialData, repository.getNameRepository().fromName(middleName).getData());
+
+ // orphan and recheck
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check data is correct
+ assertEquals(initialData, repository.getNameRepository().fromName(initialName).getData());
+ }
+ }
+
+}