primary names implementation

This commit is contained in:
kennycud 2025-05-23 17:49:26 -07:00
parent f5a4a0a16c
commit 88fe3b0af6
16 changed files with 778 additions and 6 deletions

View File

@ -2,12 +2,14 @@ package org.qortal.account;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.api.resource.TransactionsResource;
import org.qortal.block.BlockChain;
import org.qortal.controller.LiteNode;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.GroupRepository;
import org.qortal.repository.NameRepository;
@ -19,7 +21,11 @@ import org.qortal.utils.Groups;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.qortal.utils.Amounts.prettyAmount;
@ -361,6 +367,142 @@ public class Account {
return accountData.getLevel();
}
/**
* Get Primary Name
*
* @return the primary name for this address if present, otherwise empty
*
* @throws DataException
*/
public Optional<String> getPrimaryName() throws DataException {
return this.repository.getNameRepository().getPrimaryName(this.address);
}
/**
* Remove Primary Name
*
* @throws DataException
*/
public void removePrimaryName() throws DataException {
this.repository.getNameRepository().removePrimaryName(this.address);
}
/**
* Reset Primary Name
*
* Set primary name based on the names (and their history) this account owns.
*
* @param confirmationStatus the status of the transactions for the determining the primary name
*
* @return the primary name, empty if their isn't one
*
* @throws DataException
*/
public Optional<String> resetPrimaryName(TransactionsResource.ConfirmationStatus confirmationStatus) throws DataException {
Optional<String> primaryName = determinePrimaryName(confirmationStatus);
if(primaryName.isPresent()) {
return setPrimaryName(primaryName.get());
}
else {
return primaryName;
}
}
/**
* Determine Primary Name
*
* Determine primary name based on a list of registered names.
*
* @param confirmationStatus the status of the transactions for this determination
*
* @return the primary name, empty if there is no primary name
*
* @throws DataException
*/
public Optional<String> determinePrimaryName(TransactionsResource.ConfirmationStatus confirmationStatus) throws DataException {
// all registered names for the owner
List<NameData> names = this.repository.getNameRepository().getNamesByOwner(this.address);
Optional<String> primaryName;
// if no registered names, the no primary name possible
if (names.isEmpty()) {
primaryName = Optional.empty();
}
// if names
else {
// if one name, then that is the primary name
if (names.size() == 1) {
primaryName = Optional.of( names.get(0).getName() );
}
// if more than one name, then seek the earliest name acquisition that was never released
else {
Map<String, TransactionData> txByName = new HashMap<>(names.size());
// for each name, get the latest transaction
for (NameData nameData : names) {
// since the name is currently registered to the owner,
// we assume the latest transaction involving this name was the transaction that the acquired
// name through registration, purchase or update
Optional<TransactionData> latestTransaction
= this.repository
.getTransactionRepository()
.getTransactionsInvolvingName(
nameData.getName(),
confirmationStatus
)
.stream()
.sorted(Comparator.comparing(
TransactionData::getTimestamp).reversed()
)
.findFirst(); // first is the last, since it was reversed
// if there is a latest transaction, expected for all registered names
if (latestTransaction.isPresent()) {
txByName.put(nameData.getName(), latestTransaction.get());
}
// if there is no latest transaction, then
else {
LOGGER.warn("No matching transaction for name: " + nameData.getName());
}
}
// get the first name aqcuistion for this address
Optional<Map.Entry<String, TransactionData>> firstNameEntry
= txByName.entrySet().stream().sorted(Comparator.comparing(entry -> entry.getValue().getTimestamp())).findFirst();
// if their is a name acquisition, then the first one is the primary name
if (firstNameEntry.isPresent()) {
primaryName = Optional.of( firstNameEntry.get().getKey() );
}
// if there is no nameacquistion, then there is no primary name
else {
primaryName = Optional.empty();
}
}
}
return primaryName;
}
/**
* Set Primary Name
*
* @param primaryName the primary to set to this address
*
* @return the primary name if successful, empty if unsuccessful
*
* @throws DataException
*/
public Optional<String> setPrimaryName( String primaryName ) throws DataException {
int changed = this.repository.getNameRepository().setPrimaryName(this.address, primaryName);
return changed > 0 ? Optional.of(primaryName) : Optional.empty();
}
/**
* Returns reward-share minting address, or unknown if reward-share does not exist.
*

View File

@ -33,6 +33,7 @@ import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Path("/names")
@ -104,6 +105,45 @@ public class NamesResource {
}
}
@GET
@Path("/primary/{address}")
@Operation(
summary = "primary name owned by address",
responses = {
@ApiResponse(
description = "registered primary name info",
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = NameSummary.class)
)
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE, ApiError.UNAUTHORIZED})
public NameSummary getPrimaryNameByAddress(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
if (Settings.getInstance().isLite()) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
}
else {
Optional<String> primaryName = repository.getNameRepository().getPrimaryName(address);
if(primaryName.isPresent()) {
return new NameSummary(new NameData(primaryName.get(), address));
}
else {
return new NameSummary((new NameData(null, address)));
}
}
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/{name}")
@Operation(

View File

@ -1640,6 +1640,8 @@ public class Block {
SelfSponsorshipAlgoV2Block.processAccountPenalties(this);
} else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) {
SelfSponsorshipAlgoV3Block.processAccountPenalties(this);
} else if (this.blockData.getHeight() == BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
PrimaryNamesBlock.processNames(this.repository);
}
}
}
@ -1952,6 +1954,8 @@ public class Block {
SelfSponsorshipAlgoV2Block.orphanAccountPenalties(this);
} else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) {
SelfSponsorshipAlgoV3Block.orphanAccountPenalties(this);
} else if (this.blockData.getHeight() == BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
PrimaryNamesBlock.orphanNames( this.repository );
}
}

View File

@ -0,0 +1,47 @@
package org.qortal.block;
import org.qortal.account.Account;
import org.qortal.api.resource.TransactionsResource;
import org.qortal.data.naming.NameData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Class PrimaryNamesBlock
*/
public class PrimaryNamesBlock {
/**
* Process Primary Names
*
* @param repository
* @throws DataException
*/
public static void processNames(Repository repository) throws DataException {
Set<String> addressesWithNames
= repository.getNameRepository().getAllNames().stream()
.map(NameData::getOwner).collect(Collectors.toSet());
// for each address with a name, set primary name to the address
for( String address : addressesWithNames ) {
Account account = new Account(repository, address);
account.resetPrimaryName(TransactionsResource.ConfirmationStatus.CONFIRMED);
}
}
/**
* Orphan the Primary Names Block
*
* @param repository
* @throws DataException
*/
public static void orphanNames(Repository repository) throws DataException {
repository.getNameRepository().clearPrimaryNames();
}
}

View File

@ -67,6 +67,11 @@ public class NameData {
this(name, reducedName, owner, data, registered, null, false, null, reference, creationGroupId);
}
// Typically used for name summsry
public NameData(String name, String owner) {
this(name, null, owner, null, 0L, null, false, null, null, 0);
}
// Getters / setters
public String getName() {

View File

@ -3,6 +3,7 @@ package org.qortal.repository;
import org.qortal.data.naming.NameData;
import java.util.List;
import java.util.Optional;
public interface NameRepository {
@ -34,10 +35,17 @@ public interface NameRepository {
return getNamesByOwner(address, null, null, null);
}
public int setPrimaryName(String address, String primaryName) throws DataException;
public void removePrimaryName(String address) throws DataException;
public Optional<String> getPrimaryName(String address) throws DataException;
public int clearPrimaryNames() throws DataException;
public List<String> getRecentNames(long startTimestamp) throws DataException;
public void save(NameData nameData) throws DataException;
public void delete(String name) throws DataException;
}

View File

@ -1,5 +1,8 @@
package org.qortal.repository.hsqldb;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.block.BlockChain;
import org.qortal.data.chat.ActiveChats;
import org.qortal.data.chat.ActiveChats.DirectChat;
import org.qortal.data.chat.ActiveChats.GroupChat;
@ -18,6 +21,8 @@ import static org.qortal.data.chat.ChatMessage.Encoding;
public class HSQLDBChatRepository implements ChatRepository {
private static final Logger LOGGER = LogManager.getLogger(HSQLDBChatRepository.class);
protected HSQLDBRepository repository;
public HSQLDBChatRepository(HSQLDBRepository repository) {
@ -142,10 +147,23 @@ public class HSQLDBChatRepository implements ChatRepository {
@Override
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData, Encoding encoding) throws DataException {
String tableName;
// if the PrimaryTable is available, then use it
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
LOGGER.info("using PrimaryNames for chat transactions");
tableName = "PrimaryNames";
}
else {
LOGGER.info("using Names for chat transactions");
tableName = "Names";
}
String sql = "SELECT SenderNames.name, RecipientNames.name "
+ "FROM ChatTransactions "
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
+ "LEFT OUTER JOIN Names AS RecipientNames ON RecipientNames.owner = recipient "
+ "LEFT OUTER JOIN " + tableName + " AS SenderNames ON SenderNames.owner = sender "
+ "LEFT OUTER JOIN " + tableName + " AS RecipientNames ON RecipientNames.owner = recipient "
+ "WHERE signature = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, chatTransactionData.getSignature())) {

View File

@ -1053,6 +1053,12 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("UPDATE Accounts SET blocks_minted_penalty = -5000000 WHERE blocks_minted_penalty < 0");
break;
case 50:
// Primary name for a Qortal Address, 0-1 for any address
stmt.execute("CREATE TABLE PrimaryNames (owner QortalAddress, name RegisteredName, "
+ "PRIMARY KEY (owner), FOREIGN KEY (name) REFERENCES Names (name) ON DELETE CASCADE)");
break;
default:
// nothing to do
return false;

View File

@ -8,6 +8,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class HSQLDBNameRepository implements NameRepository {
@ -333,6 +334,55 @@ public class HSQLDBNameRepository implements NameRepository {
}
}
@Override
public void removePrimaryName(String address) throws DataException {
try {
this.repository.delete("PrimaryNames", "owner = ?", address);
} catch (SQLException e) {
throw new DataException("Unable to delete primary name from repository", e);
}
}
@Override
public Optional<String> getPrimaryName(String address) throws DataException {
String sql = "SELECT name FROM PrimaryNames WHERE owner = ?";
List<String> names = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
if (resultSet == null)
return Optional.empty();
String name = resultSet.getString(1);
return Optional.of(name);
} catch (SQLException e) {
throw new DataException("Unable to fetch recent names from repository", e);
}
}
@Override
public int setPrimaryName(String address, String primaryName) throws DataException {
String sql = "INSERT INTO PrimaryNames (owner, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE name = ?";
try{
return this.repository.executeCheckedUpdate(sql, address, primaryName, primaryName);
} catch (SQLException e) {
throw new DataException("Unable to set primary name", e);
}
}
@Override
public int clearPrimaryNames() throws DataException {
try {
return this.repository.delete("PrimaryNames");
} catch (SQLException e) {
throw new DataException("Unable to clear primary names from repository", e);
}
}
@Override
public void save(NameData nameData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Names");

View File

@ -16,6 +16,7 @@ import org.qortal.utils.Unicode;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class BuyNameTransaction extends Transaction {
@ -117,6 +118,25 @@ public class BuyNameTransaction extends Transaction {
// Save transaction with updated "name reference" pointing to previous transaction that changed name
this.repository.getTransactionRepository().save(this.buyNameTransactionData);
// if multiple names feature is activated, then check the buyer and seller's primary name status
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
Account seller = new Account(this.repository, this.buyNameTransactionData.getSeller());
Optional<String> sellerPrimaryName = seller.getPrimaryName();
// if the seller sold their primary name, then remove their primary name
if (sellerPrimaryName.isPresent() && sellerPrimaryName.get().equals(buyNameTransactionData.getName())) {
seller.removePrimaryName();
}
Account buyer = new Account(this.repository, this.getBuyer().getAddress());
// if the buyer had no primary name, then set the primary name to the name bought
if( buyer.getPrimaryName().isEmpty() ) {
buyer.setPrimaryName(this.buyNameTransactionData.getName());
}
}
}
@Override
@ -127,6 +147,24 @@ public class BuyNameTransaction extends Transaction {
// Save this transaction, with previous "name reference"
this.repository.getTransactionRepository().save(this.buyNameTransactionData);
}
// if multiple names feature is activated, then check the buyer and seller's primary name status
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
Account seller = new Account(this.repository, this.buyNameTransactionData.getSeller());
// if the seller lost their primary name, then set their primary name back
if (seller.getPrimaryName().isEmpty()) {
seller.setPrimaryName(this.buyNameTransactionData.getName());
}
Account buyer = new Account(this.repository, this.getBuyer().getAddress());
Optional<String> buyerPrimaryName = buyer.getPrimaryName();
// if the buyer bought their primary, then remove it
if( buyerPrimaryName.isPresent() && this.buyNameTransactionData.getName().equals(buyerPrimaryName.get()) ) {
buyer.removePrimaryName();
}
}
}
}

View File

@ -2,10 +2,12 @@ package org.qortal.transaction;
import com.google.common.base.Utf8;
import org.qortal.account.Account;
import org.qortal.api.resource.TransactionsResource;
import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
import org.qortal.crypto.Crypto;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.RegisterNameTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.naming.Name;
@ -15,6 +17,7 @@ import org.qortal.utils.Unicode;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class RegisterNameTransaction extends Transaction {
@ -54,6 +57,15 @@ public class RegisterNameTransaction extends Transaction {
Account registrant = getRegistrant();
String name = this.registerNameTransactionData.getName();
Optional<String> registrantPrimaryName = registrant.getPrimaryName();
if( registrantPrimaryName.isPresent() ) {
NameData nameData = repository.getNameRepository().fromName(registrantPrimaryName.get());
if (nameData.isForSale()) {
return ValidationResult.NOT_SUPPORTED;
}
}
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
final int start = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() - 1180;
final int end = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height();
@ -117,6 +129,16 @@ public class RegisterNameTransaction extends Transaction {
// Register Name
Name name = new Name(this.repository, this.registerNameTransactionData);
name.register();
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
Account account = new Account(this.repository, this.getCreator().getAddress());
// if there is no primary name established, then the new registered name is the primary name
if (account.getPrimaryName().isEmpty()) {
account.setPrimaryName(this.registerNameTransactionData.getName());
}
}
}
@Override

View File

@ -3,6 +3,7 @@ package org.qortal.transaction;
import com.google.common.base.Utf8;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
import org.qortal.crypto.Crypto;
import org.qortal.data.naming.NameData;
@ -49,6 +50,12 @@ public class UpdateNameTransaction extends Transaction {
public ValidationResult isValid() throws DataException {
String name = this.updateNameTransactionData.getName();
// if the account has more than one name, then they cannot update their primary name
if( this.repository.getNameRepository().getNamesByOwner(this.getOwner().getAddress()).size() > 1 &&
this.getOwner().getPrimaryName().get().equals(name) ) {
return ValidationResult.NOT_SUPPORTED;
}
// Check name size bounds
int nameLength = Utf8.encodedLength(name);
if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE)
@ -152,6 +159,16 @@ public class UpdateNameTransaction extends Transaction {
// Save this transaction, now with updated "name reference" to previous transaction that changed name
this.repository.getTransactionRepository().save(this.updateNameTransactionData);
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
Account account = new Account(this.repository, this.getCreator().getAddress());
// if updating the primary name, then set primary name to new name
if( account.getPrimaryName().isEmpty() || account.getPrimaryName().get().equals(this.updateNameTransactionData.getName())) {
account.setPrimaryName(this.updateNameTransactionData.getNewName());
}
}
}
@Override
@ -167,6 +184,16 @@ public class UpdateNameTransaction extends Transaction {
// Save this transaction, with previous "name reference"
this.repository.getTransactionRepository().save(this.updateNameTransactionData);
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
Account account = new Account(this.repository, this.getCreator().getAddress());
// if the primary name is the new updated name, then it needs to be set back to the previous name
if (account.getPrimaryName().isPresent() && account.getPrimaryName().get().equals(this.updateNameTransactionData.getNewName())) {
account.setPrimaryName(this.updateNameTransactionData.getName());
}
}
}
}

View File

@ -4,6 +4,7 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.api.resource.TransactionsResource;
import org.qortal.block.BlockChain;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.BuyNameTransactionData;
@ -22,6 +23,7 @@ import org.qortal.transaction.Transaction;
import org.qortal.utils.Amounts;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import static org.junit.Assert.*;
@ -135,13 +137,26 @@ public class BuySellTests extends Common {
@Test
public void testSellName() throws DataException {
// mint passed the feature trigger block
BlockUtils.mintBlocks(repository, BlockChain.getInstance().getMultipleNamesPerAccountHeight());
// Register-name
testRegisterName();
// assert primary name for alice
Optional<String> alicePrimaryName1 = alice.getPrimaryName();
assertTrue(alicePrimaryName1.isPresent());
assertTrue(alicePrimaryName1.get().equals(name));
// Sell-name
SellNameTransactionData transactionData = new SellNameTransactionData(TestTransaction.generateBase(alice), name, price);
TransactionUtils.signAndMint(repository, transactionData, alice);
// assert primary name for alice
Optional<String> alicePrimaryName2 = alice.getPrimaryName();
assertTrue(alicePrimaryName2.isPresent());
assertTrue(alicePrimaryName2.get().equals(name));
NameData nameData;
// Check name is for sale
@ -149,6 +164,14 @@ public class BuySellTests extends Common {
assertTrue(nameData.isForSale());
assertEquals("price incorrect", price, nameData.getSalePrice());
// assert alice cannot register another name while primary name is for sale
final String name2 = "another name";
RegisterNameTransactionData registerSecondNameData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name2, "{}");
Transaction.ValidationResult registrationResult = TransactionUtils.signAndImport(repository, registerSecondNameData, alice);
// check that registering is not supported while primary name is for sale
assertTrue(Transaction.ValidationResult.NOT_SUPPORTED.equals(registrationResult));
// Orphan sell-name
BlockUtils.orphanLastBlock(repository);
@ -168,6 +191,10 @@ public class BuySellTests extends Common {
// Orphan sell-name and register-name
BlockUtils.orphanBlocks(repository, 2);
// assert primary name for alice
Optional<String> alicePrimaryName3 = alice.getPrimaryName();
assertTrue(alicePrimaryName3.isEmpty());
// Check name no longer exists
assertFalse(repository.getNameRepository().nameExists(name));
nameData = repository.getNameRepository().fromName(name);
@ -261,15 +288,36 @@ public class BuySellTests extends Common {
@Test
public void testBuyName() throws DataException {
// move passed primary initiation
BlockUtils.mintBlocks(repository, BlockChain.getInstance().getMultipleNamesPerAccountHeight());
// Register-name and sell-name
testSellName();
String seller = alice.getAddress();
// assert alice has the name as primary
Optional<String> alicePrimaryName1 = alice.getPrimaryName();
assertTrue(alicePrimaryName1.isPresent());
assertEquals(name, alicePrimaryName1.get());
// assert bob does not have a primary name
Optional<String> bobPrimaryName1 = bob.getPrimaryName();
assertTrue(bobPrimaryName1.isEmpty());
// Buy-name
BuyNameTransactionData transactionData = new BuyNameTransactionData(TestTransaction.generateBase(bob), name, price, seller);
TransactionUtils.signAndMint(repository, transactionData, bob);
// assert alice does not have a primary name anymore
Optional<String> alicePrimaryName2 = alice.getPrimaryName();
assertTrue(alicePrimaryName2.isEmpty());
// assert bob does have the name as primary
Optional<String> bobPrimaryName2 = bob.getPrimaryName();
assertTrue(bobPrimaryName2.isPresent());
assertEquals(name, bobPrimaryName2.get());
NameData nameData;
// Check name is sold
@ -280,6 +328,15 @@ public class BuySellTests extends Common {
// Orphan buy-name
BlockUtils.orphanLastBlock(repository);
// assert alice has the name as primary
Optional<String> alicePrimaryNameOrphaned = alice.getPrimaryName();
assertTrue(alicePrimaryNameOrphaned.isPresent());
assertEquals(name, alicePrimaryNameOrphaned.get());
// assert bob does not have a primary name
Optional<String> bobPrimaryNameOrphaned = bob.getPrimaryName();
assertTrue(bobPrimaryNameOrphaned.isEmpty());
// Check name is for sale (not sold)
nameData = repository.getNameRepository().fromName(name);
assertTrue(nameData.isForSale());
@ -314,6 +371,9 @@ public class BuySellTests extends Common {
assertFalse(nameData.isForSale());
// Not concerned about price
assertEquals(bob.getAddress(), nameData.getOwner());
assertEquals(alice.getPrimaryName(), alice.determinePrimaryName(TransactionsResource.ConfirmationStatus.CONFIRMED));
assertEquals(bob.getPrimaryName(), bob.determinePrimaryName(TransactionsResource.ConfirmationStatus.CONFIRMED));
}
@Test
@ -373,6 +433,9 @@ public class BuySellTests extends Common {
assertTrue(nameData.isForSale());
assertEquals("price incorrect", newPrice, nameData.getSalePrice());
assertEquals(bob.getAddress(), nameData.getOwner());
assertEquals(alice.getPrimaryName(), alice.determinePrimaryName(TransactionsResource.ConfirmationStatus.CONFIRMED));
assertEquals(bob.getPrimaryName(), bob.determinePrimaryName(TransactionsResource.ConfirmationStatus.CONFIRMED));
}
}

View File

@ -4,6 +4,8 @@ import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.api.resource.TransactionsResource;
import org.qortal.block.BlockChain;
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.*;
@ -13,6 +15,7 @@ import org.qortal.repository.RepositoryFactory;
import org.qortal.repository.RepositoryManager;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.settings.Settings;
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;
@ -385,6 +388,8 @@ public class IntegrityTests extends Common {
@Test
public void testUpdateToMissingName() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockUtils.mintBlocks(repository, BlockChain.getInstance().getMultipleNamesPerAccountHeight());
// Register-name
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String initialName = "test-name";
@ -422,7 +427,12 @@ public class IntegrityTests extends Common {
// Therefore the name that we are trying to rename TO already exists
Transaction.ValidationResult result = transaction.importAsUnconfirmed();
assertTrue("Transaction should be invalid", Transaction.ValidationResult.OK != result);
assertTrue("Destination name should already exist", Transaction.ValidationResult.NAME_ALREADY_REGISTERED == result);
// this assertion has been updated, because the primary name logic now comes into play and you cannot update a primary name when there
// is other names registered and if your try a NOT SUPPORTED result will be given
assertTrue("Destination name should already exist", Transaction.ValidationResult.NOT_SUPPORTED == result);
assertEquals(alice.getPrimaryName(), alice.determinePrimaryName(TransactionsResource.ConfirmationStatus.CONFIRMED));
}
}

View File

@ -1,6 +1,7 @@
package org.qortal.test.naming;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
@ -8,6 +9,7 @@ import org.qortal.api.AmountTypeAdapter;
import org.qortal.block.BlockChain;
import org.qortal.block.BlockChain.UnitFeesByTimestamp;
import org.qortal.controller.BlockMinter;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.PaymentTransactionData;
import org.qortal.data.transaction.RegisterNameTransactionData;
import org.qortal.data.transaction.TransactionData;
@ -28,6 +30,7 @@ import org.qortal.utils.NTP;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.junit.Assert.*;
@ -121,6 +124,8 @@ public class MiscTests extends Common {
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
TransactionUtils.signAndMint(repository, transactionData, alice);
BlockUtils.mintBlocks(repository, BlockChain.getInstance().getMultipleNamesPerAccountHeight());
// Register another name that we will later attempt to rename to first name (above)
String otherName = "new-name";
String otherData = "";
@ -335,6 +340,8 @@ public class MiscTests extends Common {
public void testRegisterNameFeeIncrease() throws Exception {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockUtils.mintBlocks(repository, BlockChain.getInstance().getMultipleNamesPerAccountHeight());
// Add original fee to nameRegistrationUnitFees
UnitFeesByTimestamp originalFee = new UnitFeesByTimestamp();
originalFee.timestamp = 0L;
@ -517,4 +524,168 @@ public class MiscTests extends Common {
}
}
}
@Test
public void testPrimaryNameEmpty() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
// mint passed the feature trigger block
BlockUtils.mintBlocks(repository, BlockChain.getInstance().getMultipleNamesPerAccountHeight());
Optional<String> primaryName = repository.getNameRepository().getPrimaryName(alice.getAddress());
Assert.assertNotNull(primaryName);
Assert.assertTrue(primaryName.isEmpty());
}
}
@Test
public void testPrimaryNameSingle() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
String name = "alice 1";
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
// register name 1
RegisterNameTransactionData transactionData1 = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
transactionData1.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData1.getTimestamp()));
TransactionUtils.signAndMint(repository, transactionData1, alice);
String name1 = transactionData1.getName();
// check name does exist
assertTrue(repository.getNameRepository().nameExists(name1));
// mint passed the feature trigger block
BlockUtils.mintBlocks(repository, BlockChain.getInstance().getMultipleNamesPerAccountHeight() + 1);
Optional<String> primaryName = repository.getNameRepository().getPrimaryName(alice.getAddress());
Assert.assertNotNull(primaryName);
Assert.assertTrue(primaryName.isPresent());
Assert.assertEquals(name, primaryName.get());
}
}
@Test
public void testPrimaryNameSingleAfterFeature() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
String name = "alice 1";
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
// mint passed the feature trigger block
BlockUtils.mintBlocks(repository, BlockChain.getInstance().getMultipleNamesPerAccountHeight());
// register name 1
RegisterNameTransactionData transactionData1 = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
transactionData1.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData1.getTimestamp()));
TransactionUtils.signAndMint(repository, transactionData1, alice);
String name1 = transactionData1.getName();
// check name does exist
assertTrue(repository.getNameRepository().nameExists(name1));
Optional<String> primaryName = repository.getNameRepository().getPrimaryName(alice.getAddress());
Assert.assertNotNull(primaryName);
Assert.assertTrue(primaryName.isPresent());
Assert.assertEquals(name, primaryName.get());
BlockUtils.orphanLastBlock(repository);
Optional<String> primaryNameOrpaned = repository.getNameRepository().getPrimaryName(alice.getAddress());
Assert.assertNotNull(primaryNameOrpaned);
Assert.assertTrue(primaryNameOrpaned.isEmpty());
}
}
@Test
public void testUpdateNameMultiple() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
String name = "alice 1";
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
// register name 1
RegisterNameTransactionData transactionData1 = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
transactionData1.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData1.getTimestamp()));
TransactionUtils.signAndMint(repository, transactionData1, alice);
String name1 = transactionData1.getName();
// check name does exist
assertTrue(repository.getNameRepository().nameExists(name1));
// register another name, second registered name should fail before the feature trigger
final String name2 = "another name";
RegisterNameTransactionData transactionData2 = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name2, "{}");
Transaction.ValidationResult resultBeforeFeatureTrigger = TransactionUtils.signAndImport(repository, transactionData2, alice);
// check that that multiple names is forbidden
assertTrue(Transaction.ValidationResult.MULTIPLE_NAMES_FORBIDDEN.equals(resultBeforeFeatureTrigger));
// mint passed the feature trigger block
BlockUtils.mintBlocks(repository, BlockChain.getInstance().getMultipleNamesPerAccountHeight());
// register again, now that we are passed the feature trigger
RegisterNameTransactionData transactionData3 = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name2, "{}");
Transaction.ValidationResult resultAfterFeatureTrigger = TransactionUtils.signAndImport(repository, transactionData3, alice);
// check that multiple names is ok
assertTrue(Transaction.ValidationResult.OK.equals(resultAfterFeatureTrigger));
// mint block, confirm transaction
BlockUtils.mintBlock(repository);
// check name does exist
assertTrue(repository.getNameRepository().nameExists(name2));
// check that there are 2 names for one account
List<NameData> namesByOwner = repository.getNameRepository().getNamesByOwner(alice.getAddress(), 0, 0, false);
assertEquals(2, namesByOwner.size());
// check that the order is correct
assertEquals(name1, namesByOwner.get(0).getName());
String newestName = "newest-name";
String newestReducedName = "newest-name";
String newestData = "newest-data";
TransactionData newestTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name2, newestName, newestData);
TransactionUtils.signAndMint(repository, newestTransactionData, alice);
// Check previous name no longer exists
assertFalse(repository.getNameRepository().nameExists(name2));
// Check newest name exists
assertTrue(repository.getNameRepository().nameExists(newestName));
Optional<String> alicePrimaryName1 = alice.getPrimaryName();
assertTrue( alicePrimaryName1.isPresent() );
assertEquals( name1, alicePrimaryName1.get() );
// orphan and recheck
BlockUtils.orphanLastBlock(repository);
Optional<String> alicePrimaryName2 = alice.getPrimaryName();
assertTrue( alicePrimaryName2.isPresent() );
assertEquals( name1, alicePrimaryName2.get() );
// Check newest name no longer exists
assertFalse(repository.getNameRepository().nameExists(newestName));
assertNull(repository.getNameRepository().fromReducedName(newestReducedName));
// Check previous name exists again
assertTrue(repository.getNameRepository().nameExists(name2));
}
}
}

View File

@ -3,8 +3,12 @@ package org.qortal.test.naming;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.api.resource.TransactionsResource;
import org.qortal.block.BlockChain;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.BuyNameTransactionData;
import org.qortal.data.transaction.RegisterNameTransactionData;
import org.qortal.data.transaction.SellNameTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.UpdateNameTransactionData;
import org.qortal.repository.DataException;
@ -15,6 +19,9 @@ import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.RegisterNameTransaction;
import org.qortal.transaction.Transaction;
import java.util.Optional;
import static org.junit.Assert.*;
@ -395,6 +402,13 @@ public class UpdateTests extends Common {
assertTrue(repository.getNameRepository().nameExists(initialName));
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
// move passed primary initiation
BlockUtils.mintBlocks(repository, BlockChain.getInstance().getMultipleNamesPerAccountHeight());
// check primary name
assertTrue(alice.getPrimaryName().isPresent());
assertEquals(initialName, alice.getPrimaryName().get());
// Update data
String middleName = "middle-name";
String middleReducedName = "midd1e-name";
@ -402,6 +416,11 @@ public class UpdateTests extends Common {
transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData);
TransactionUtils.signAndMint(repository, transactionData, alice);
// check primary name
Optional<String> alicePrimaryName1 = alice.getPrimaryName();
assertTrue(alicePrimaryName1.isPresent());
assertEquals(middleName, alicePrimaryName1.get());
// Check data is correct
assertEquals(middleData, repository.getNameRepository().fromName(middleName).getData());
@ -414,6 +433,11 @@ public class UpdateTests extends Common {
// Check data is correct
assertEquals(newestData, repository.getNameRepository().fromName(newestName).getData());
// check primary name
Optional<String> alicePrimaryName2 = alice.getPrimaryName();
assertTrue(alicePrimaryName2.isPresent());
assertEquals(newestName, alicePrimaryName2.get());
// Check initial name no longer exists
assertFalse(repository.getNameRepository().nameExists(initialName));
assertNull(repository.getNameRepository().fromReducedName(initialReducedName));
@ -516,4 +540,101 @@ public class UpdateTests extends Common {
}
}
@Test
public void testUpdatePrimaryName() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
// mint passed the feature trigger block
BlockUtils.mintBlocks(repository, BlockChain.getInstance().getMultipleNamesPerAccountHeight());
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
// register name 1
String initialName = "initial-name";
RegisterNameTransactionData registerNameTransactionData1 = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, "{}");
registerNameTransactionData1.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData1.getTimestamp()));
TransactionUtils.signAndMint(repository, registerNameTransactionData1, alice);
// assert name 1 registration, assert primary name
assertTrue(repository.getNameRepository().nameExists(initialName));
Optional<String> primaryNameOptional = alice.getPrimaryName();
assertTrue(primaryNameOptional.isPresent());
assertEquals(initialName, primaryNameOptional.get());
// register name 2
String secondName = "second-name";
RegisterNameTransactionData registerNameTransactionData2 = new RegisterNameTransactionData(TestTransaction.generateBase(alice), secondName, "{}");
registerNameTransactionData2.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData2.getTimestamp()));
TransactionUtils.signAndMint(repository, registerNameTransactionData2, alice);
// assert name 2 registration, assert primary has not changed
assertTrue(repository.getNameRepository().nameExists(secondName));
// the name alice is trying to update to
String newName = "updated-name";
// update name, assert invalid
updateName(repository, initialName, newName, Transaction.ValidationResult.NOT_SUPPORTED, alice);
// check primary name did not update
// check primary name update
Optional<String> primaryNameNotUpdateOptional = alice.getPrimaryName();
assertTrue(primaryNameNotUpdateOptional.isPresent());
assertEquals(initialName, primaryNameNotUpdateOptional.get());
// sell name 2, assert valid
Long amount = 1000000L;
SellNameTransactionData transactionData = new SellNameTransactionData(TestTransaction.generateBase(alice), secondName, amount);
TransactionUtils.signAndMint(repository, transactionData, alice);
// Check name is for sale
NameData nameData = repository.getNameRepository().fromName(secondName);
assertTrue(nameData.isForSale());
assertEquals("price incorrect", amount, nameData.getSalePrice());
// bob buys name 2, assert
BuyNameTransactionData bobBuysName2Data = new BuyNameTransactionData(TestTransaction.generateBase(bob), secondName, amount, alice.getAddress());
TransactionUtils.signAndMint(repository, bobBuysName2Data, bob);
// update name, assert valid, assert primary name change
updateName(repository, initialName, newName, Transaction.ValidationResult.OK, alice);
// check primary name update
Optional<String> primaryNameUpdateOptional = alice.getPrimaryName();
assertTrue(primaryNameUpdateOptional.isPresent());
assertEquals(newName, primaryNameUpdateOptional.get());
assertEquals(alice.getPrimaryName(), alice.determinePrimaryName(TransactionsResource.ConfirmationStatus.CONFIRMED));
assertEquals(bob.getPrimaryName(), bob.determinePrimaryName(TransactionsResource.ConfirmationStatus.CONFIRMED));
}
}
/**
* Update Name
*
* @param repository
* @param initialName the name before the update
* @param newName the name after the update
* @param expectedValidationResult the validation result expected from the update
* @param account the account for the update
*
* @throws DataException
*/
private static void updateName(Repository repository, String initialName, String newName, Transaction.ValidationResult expectedValidationResult, PrivateKeyAccount account) throws DataException {
TransactionData data = new UpdateNameTransactionData(TestTransaction.generateBase(account), initialName, newName, "{}");
Transaction.ValidationResult result = TransactionUtils.signAndImport(repository,data, account);
assertEquals("Transaction invalid", expectedValidationResult, result);
BlockUtils.mintBlock(repository);
if( Transaction.ValidationResult.OK.equals(expectedValidationResult) ) {
assertTrue(repository.getNameRepository().nameExists(newName));
}
else {
// the new name should not exist, because the update was invalid
assertFalse(repository.getNameRepository().nameExists(newName));
}
}
}